dsh-vidi 0.4.3 → 0.5.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 +565 -143
- package/lib/index.mjs +38 -13
- package/package.json +1 -1
package/lib/client.js
CHANGED
|
@@ -776,6 +776,194 @@ function langLabel(code, ui, fallback) {
|
|
|
776
776
|
return NAMES[code]?.[ui] ?? fallback ?? code;
|
|
777
777
|
}
|
|
778
778
|
|
|
779
|
+
// src/client/Pick.tsx
|
|
780
|
+
/** Theme tokens copied onto the list, so it looks like the place it opened from (light page, dark player menu). */
|
|
781
|
+
const TOKENS = [
|
|
782
|
+
"--bg",
|
|
783
|
+
"--l1",
|
|
784
|
+
"--l2",
|
|
785
|
+
"--ink",
|
|
786
|
+
"--muted",
|
|
787
|
+
"--faint",
|
|
788
|
+
"--line",
|
|
789
|
+
"--hover",
|
|
790
|
+
"--accent"
|
|
791
|
+
];
|
|
792
|
+
/**
|
|
793
|
+
* A drop-down in the page's own style, in place of the browser's <select>
|
|
794
|
+
* (which draws a system menu as wide as the screen allows). The list is sized
|
|
795
|
+
* to its options, scrolls when long, opens upward when there is no room
|
|
796
|
+
* below, and works with the keyboard (arrows, Enter, Esc, typing a letter).
|
|
797
|
+
*/
|
|
798
|
+
function Pick(props) {
|
|
799
|
+
const { value, options, onChange, placeholder, disabled, className, title, label } = props;
|
|
800
|
+
const [open, setOpen] = (0, react.useState)(false);
|
|
801
|
+
const [active, setActive] = (0, react.useState)(-1);
|
|
802
|
+
const [place, setPlace] = (0, react.useState)({});
|
|
803
|
+
const btn = (0, react.useRef)(null);
|
|
804
|
+
const list = (0, react.useRef)(null);
|
|
805
|
+
const chosen = options.find((o) => o.value === value);
|
|
806
|
+
const usable = options.map((o, i) => o.disabled ? -1 : i).filter((i) => i >= 0);
|
|
807
|
+
const close = (focus = true) => {
|
|
808
|
+
setOpen(false);
|
|
809
|
+
if (focus) btn.current?.focus();
|
|
810
|
+
};
|
|
811
|
+
const choose = (o) => {
|
|
812
|
+
if (o.disabled) return;
|
|
813
|
+
onChange(o.value);
|
|
814
|
+
close();
|
|
815
|
+
};
|
|
816
|
+
(0, react.useLayoutEffect)(() => {
|
|
817
|
+
if (!open || !btn.current) return;
|
|
818
|
+
const r = btn.current.getBoundingClientRect();
|
|
819
|
+
const cs = getComputedStyle(btn.current);
|
|
820
|
+
const vars = Object.fromEntries(TOKENS.map((k) => [k, cs.getPropertyValue(k).trim()]).filter(([, v]) => v));
|
|
821
|
+
const room = window.innerHeight - r.bottom - 8, above = r.top - 8;
|
|
822
|
+
const up = room < 220 && above > room;
|
|
823
|
+
const maxH = Math.max(120, Math.min(320, up ? above - 4 : room - 4));
|
|
824
|
+
const width = Math.min(Math.max(r.width, 180), window.innerWidth - 16);
|
|
825
|
+
const left = Math.min(Math.max(8, r.left), window.innerWidth - width - 8);
|
|
826
|
+
setPlace({
|
|
827
|
+
...vars,
|
|
828
|
+
left,
|
|
829
|
+
width,
|
|
830
|
+
maxHeight: maxH,
|
|
831
|
+
...up ? { bottom: window.innerHeight - r.top + 4 } : { top: r.bottom + 4 },
|
|
832
|
+
fontFamily: cs.fontFamily
|
|
833
|
+
});
|
|
834
|
+
setActive(Math.max(0, options.findIndex((o) => o.value === value)));
|
|
835
|
+
}, [open]);
|
|
836
|
+
(0, react.useEffect)(() => {
|
|
837
|
+
list.current?.querySelector(`[data-i="${active}"]`)?.scrollIntoView({ block: "nearest" });
|
|
838
|
+
}, [active, open]);
|
|
839
|
+
(0, react.useEffect)(() => {
|
|
840
|
+
if (!open) return;
|
|
841
|
+
const away = (e) => {
|
|
842
|
+
if (!list.current?.contains(e.target) && !btn.current?.contains(e.target)) close(false);
|
|
843
|
+
};
|
|
844
|
+
const gone = (e) => {
|
|
845
|
+
if (!list.current?.contains(e.target)) close(false);
|
|
846
|
+
};
|
|
847
|
+
document.addEventListener("pointerdown", away, true);
|
|
848
|
+
window.addEventListener("resize", gone);
|
|
849
|
+
window.addEventListener("scroll", gone, true);
|
|
850
|
+
return () => {
|
|
851
|
+
document.removeEventListener("pointerdown", away, true);
|
|
852
|
+
window.removeEventListener("resize", gone);
|
|
853
|
+
window.removeEventListener("scroll", gone, true);
|
|
854
|
+
};
|
|
855
|
+
}, [open]);
|
|
856
|
+
const onKey = (e) => {
|
|
857
|
+
if (disabled) return;
|
|
858
|
+
const step = (d) => {
|
|
859
|
+
const at = usable.indexOf(active);
|
|
860
|
+
setActive(usable[Math.min(usable.length - 1, Math.max(0, (at < 0 ? d > 0 ? -1 : usable.length : at) + d))] ?? -1);
|
|
861
|
+
};
|
|
862
|
+
if (!open) {
|
|
863
|
+
if ([
|
|
864
|
+
"ArrowDown",
|
|
865
|
+
"ArrowUp",
|
|
866
|
+
"Enter",
|
|
867
|
+
" "
|
|
868
|
+
].includes(e.key)) {
|
|
869
|
+
e.preventDefault();
|
|
870
|
+
setOpen(true);
|
|
871
|
+
}
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
if (e.key === "Escape") {
|
|
875
|
+
e.preventDefault();
|
|
876
|
+
e.stopPropagation();
|
|
877
|
+
close();
|
|
878
|
+
} else if (e.key === "ArrowDown") {
|
|
879
|
+
e.preventDefault();
|
|
880
|
+
step(1);
|
|
881
|
+
} else if (e.key === "ArrowUp") {
|
|
882
|
+
e.preventDefault();
|
|
883
|
+
step(-1);
|
|
884
|
+
} else if (e.key === "Home") {
|
|
885
|
+
e.preventDefault();
|
|
886
|
+
setActive(usable[0] ?? -1);
|
|
887
|
+
} else if (e.key === "End") {
|
|
888
|
+
e.preventDefault();
|
|
889
|
+
setActive(usable.at(-1) ?? -1);
|
|
890
|
+
} else if (e.key === "Enter" || e.key === " ") {
|
|
891
|
+
e.preventDefault();
|
|
892
|
+
const o = options[active];
|
|
893
|
+
if (o) choose(o);
|
|
894
|
+
} else if (e.key === "Tab") close(false);
|
|
895
|
+
else if (e.key.length === 1) {
|
|
896
|
+
const k = e.key.toLowerCase();
|
|
897
|
+
const from = usable.indexOf(active);
|
|
898
|
+
const hit = [...usable.slice(from + 1), ...usable.slice(0, from + 1)].find((i) => options[i].label.toLowerCase().startsWith(k));
|
|
899
|
+
if (hit !== void 0) setActive(hit);
|
|
900
|
+
}
|
|
901
|
+
};
|
|
902
|
+
let lastGroup;
|
|
903
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
904
|
+
ref: btn,
|
|
905
|
+
type: "button",
|
|
906
|
+
className: `dshl-select ${open ? "open" : ""} ${className ?? ""}`,
|
|
907
|
+
disabled,
|
|
908
|
+
title,
|
|
909
|
+
"aria-haspopup": "listbox",
|
|
910
|
+
"aria-expanded": open,
|
|
911
|
+
onClick: () => setOpen((v) => !v),
|
|
912
|
+
onKeyDown: onKey,
|
|
913
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
914
|
+
className: "v",
|
|
915
|
+
children: label ?? chosen?.label ?? placeholder ?? ""
|
|
916
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", {
|
|
917
|
+
className: "caret",
|
|
918
|
+
"aria-hidden": "true"
|
|
919
|
+
})]
|
|
920
|
+
}), open && (0, react_dom.createPortal)(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
921
|
+
ref: list,
|
|
922
|
+
className: "dshl-options",
|
|
923
|
+
role: "listbox",
|
|
924
|
+
style: place,
|
|
925
|
+
onKeyDown: onKey,
|
|
926
|
+
children: options.map((o, i) => {
|
|
927
|
+
const head = o.group && o.group !== lastGroup ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
928
|
+
className: "grp",
|
|
929
|
+
children: o.group
|
|
930
|
+
}, `g:${o.group}`) : null;
|
|
931
|
+
lastGroup = o.group;
|
|
932
|
+
return [head, /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
933
|
+
"data-i": i,
|
|
934
|
+
role: "option",
|
|
935
|
+
"aria-selected": o.value === value,
|
|
936
|
+
"aria-disabled": o.disabled || void 0,
|
|
937
|
+
className: `opt ${o.value === value ? "on" : ""} ${i === active ? "act" : ""} ${o.disabled ? "off" : ""}`,
|
|
938
|
+
onPointerEnter: () => !o.disabled && setActive(i),
|
|
939
|
+
onClick: () => choose(o),
|
|
940
|
+
children: [
|
|
941
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
942
|
+
className: "l",
|
|
943
|
+
children: o.label
|
|
944
|
+
}),
|
|
945
|
+
o.hint && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
946
|
+
className: "h",
|
|
947
|
+
children: o.hint
|
|
948
|
+
}),
|
|
949
|
+
o.value === value && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("svg", {
|
|
950
|
+
className: "ok",
|
|
951
|
+
viewBox: "0 0 24 24",
|
|
952
|
+
width: "14",
|
|
953
|
+
height: "14",
|
|
954
|
+
fill: "none",
|
|
955
|
+
stroke: "currentColor",
|
|
956
|
+
strokeWidth: "2.4",
|
|
957
|
+
strokeLinecap: "round",
|
|
958
|
+
strokeLinejoin: "round",
|
|
959
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("path", { d: "M5 12.5l4.5 4.5L19 7.5" })
|
|
960
|
+
})
|
|
961
|
+
]
|
|
962
|
+
}, o.value)];
|
|
963
|
+
})
|
|
964
|
+
}), document.fullscreenElement ?? document.body)] });
|
|
965
|
+
}
|
|
966
|
+
|
|
779
967
|
// src/client/LineChips.tsx
|
|
780
968
|
const MAX_TRANSLATIONS = 4;
|
|
781
969
|
/**
|
|
@@ -810,20 +998,18 @@ function LineChips(props) {
|
|
|
810
998
|
children: [i >= 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", { children: i + 1 }), label(k)]
|
|
811
999
|
}, k);
|
|
812
1000
|
}),
|
|
813
|
-
translations.length < MAX_TRANSLATIONS && /* @__PURE__ */ (0, react_jsx_runtime.
|
|
1001
|
+
translations.length < MAX_TRANSLATIONS && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pick, {
|
|
814
1002
|
className: "chip add",
|
|
815
1003
|
value: "",
|
|
816
|
-
|
|
817
|
-
if (e.target.value) onChange([...lines, e.target.value]);
|
|
818
|
-
},
|
|
1004
|
+
label: t("sub.addLang"),
|
|
819
1005
|
title: t("in.addLang"),
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
1006
|
+
onChange: (v) => {
|
|
1007
|
+
if (v) onChange([...lines, v]);
|
|
1008
|
+
},
|
|
1009
|
+
options: options.filter(([k]) => !lines.includes(k)).map(([k, v]) => ({
|
|
824
1010
|
value: k,
|
|
825
|
-
|
|
826
|
-
}
|
|
1011
|
+
label: v
|
|
1012
|
+
}))
|
|
827
1013
|
}),
|
|
828
1014
|
lines.length > 1 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
829
1015
|
className: "chip ghost",
|
|
@@ -860,30 +1046,26 @@ function SubtitleMenu(props) {
|
|
|
860
1046
|
const second = lines[1] ?? "";
|
|
861
1047
|
const label = (k) => site.find((s) => s.key === k)?.label ?? name(k);
|
|
862
1048
|
const inUse = lines.filter((l) => l !== "src" && !l.startsWith("site:"));
|
|
863
|
-
const choices =
|
|
864
|
-
|
|
1049
|
+
const choices = [
|
|
1050
|
+
{
|
|
865
1051
|
value: "src",
|
|
866
|
-
|
|
867
|
-
}
|
|
868
|
-
inUse.map((k) =>
|
|
1052
|
+
label: label("src")
|
|
1053
|
+
},
|
|
1054
|
+
...inUse.map((k) => ({
|
|
869
1055
|
value: k,
|
|
870
|
-
|
|
871
|
-
}
|
|
872
|
-
site.
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
children: v
|
|
884
|
-
}, k))
|
|
885
|
-
})
|
|
886
|
-
] });
|
|
1056
|
+
label: label(k)
|
|
1057
|
+
})),
|
|
1058
|
+
...site.map((s) => ({
|
|
1059
|
+
value: s.key,
|
|
1060
|
+
label: s.label,
|
|
1061
|
+
group: t("sub.siteGroup")
|
|
1062
|
+
})),
|
|
1063
|
+
...options.filter(([k]) => !inUse.includes(k)).map(([k, v]) => ({
|
|
1064
|
+
value: k,
|
|
1065
|
+
label: v,
|
|
1066
|
+
group: t("sub.translateTo")
|
|
1067
|
+
}))
|
|
1068
|
+
];
|
|
887
1069
|
const setMain = (v) => onLines(v === second ? [v, main] : second ? [v, second] : [v]);
|
|
888
1070
|
const setSecond = (v) => onLines(!v ? [main] : v === main ? [main] : [main, v]);
|
|
889
1071
|
const first = style.lines.find((l) => l.show)?.size ?? SIZES.m[0];
|
|
@@ -903,21 +1085,21 @@ function SubtitleMenu(props) {
|
|
|
903
1085
|
children: [
|
|
904
1086
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
905
1087
|
className: "slot",
|
|
906
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sub.main") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
1088
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sub.main") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pick, {
|
|
907
1089
|
value: main,
|
|
908
|
-
onChange:
|
|
909
|
-
|
|
1090
|
+
onChange: setMain,
|
|
1091
|
+
options: choices
|
|
910
1092
|
})]
|
|
911
1093
|
}),
|
|
912
1094
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
913
1095
|
className: "slot",
|
|
914
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sub.second") }), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
1096
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("sub.second") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pick, {
|
|
915
1097
|
value: second,
|
|
916
|
-
onChange:
|
|
917
|
-
|
|
1098
|
+
onChange: setSecond,
|
|
1099
|
+
options: [{
|
|
918
1100
|
value: "",
|
|
919
|
-
|
|
920
|
-
}
|
|
1101
|
+
label: t("sub.none")
|
|
1102
|
+
}, ...choices]
|
|
921
1103
|
})]
|
|
922
1104
|
}),
|
|
923
1105
|
second && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
@@ -1016,13 +1198,13 @@ function LoginDialog(props) {
|
|
|
1016
1198
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("login.body") }),
|
|
1017
1199
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
1018
1200
|
className: "srow",
|
|
1019
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("login.from") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
1201
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("login.from") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pick, {
|
|
1020
1202
|
value: browser,
|
|
1021
|
-
onChange:
|
|
1022
|
-
|
|
1203
|
+
onChange: onBrowser,
|
|
1204
|
+
options: browsers.map((b) => ({
|
|
1023
1205
|
value: b.id,
|
|
1024
|
-
|
|
1025
|
-
}
|
|
1206
|
+
label: b.label
|
|
1207
|
+
}))
|
|
1026
1208
|
})]
|
|
1027
1209
|
}),
|
|
1028
1210
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -33343,11 +33525,31 @@ const Player = (0, react.forwardRef)(function Player(props, ref) {
|
|
|
33343
33525
|
const idleTimer = (0, react.useRef)();
|
|
33344
33526
|
/** Where playback was, so a new source (quality switch) continues from there. */
|
|
33345
33527
|
const lastTime = (0, react.useRef)(0);
|
|
33528
|
+
const [held, setHeld] = (0, react.useState)(null);
|
|
33529
|
+
const heldRef = (0, react.useRef)(false);
|
|
33346
33530
|
(0, react.useImperativeHandle)(ref, () => ({
|
|
33347
33531
|
seek: (time) => {
|
|
33348
33532
|
if (video.current) video.current.currentTime = Math.max(0, time);
|
|
33349
33533
|
},
|
|
33350
|
-
closeMenu: () => setMenu(null)
|
|
33534
|
+
closeMenu: () => setMenu(null),
|
|
33535
|
+
hold: (text, onSkip) => {
|
|
33536
|
+
const el = video.current;
|
|
33537
|
+
if (!el) return;
|
|
33538
|
+
if (text) {
|
|
33539
|
+
if (!heldRef.current) {
|
|
33540
|
+
heldRef.current = true;
|
|
33541
|
+
el.pause();
|
|
33542
|
+
}
|
|
33543
|
+
setHeld((h) => h?.text === text ? h : {
|
|
33544
|
+
text,
|
|
33545
|
+
skip: onSkip
|
|
33546
|
+
});
|
|
33547
|
+
} else if (heldRef.current) {
|
|
33548
|
+
heldRef.current = false;
|
|
33549
|
+
setHeld(null);
|
|
33550
|
+
el.play().catch(() => {});
|
|
33551
|
+
}
|
|
33552
|
+
}
|
|
33351
33553
|
}), []);
|
|
33352
33554
|
(0, react.useEffect)(() => {
|
|
33353
33555
|
const el = video.current;
|
|
@@ -33366,8 +33568,7 @@ const Player = (0, react.forwardRef)(function Player(props, ref) {
|
|
|
33366
33568
|
backBufferLength: Infinity
|
|
33367
33569
|
} : {
|
|
33368
33570
|
liveSyncDuration: delay,
|
|
33369
|
-
|
|
33370
|
-
backBufferLength: 1800
|
|
33571
|
+
backBufferLength: Infinity
|
|
33371
33572
|
});
|
|
33372
33573
|
hls.current = h;
|
|
33373
33574
|
h.loadSource(src);
|
|
@@ -33445,7 +33646,7 @@ const Player = (0, react.forwardRef)(function Player(props, ref) {
|
|
|
33445
33646
|
(0, react.useEffect)(() => {
|
|
33446
33647
|
if (!menu) return;
|
|
33447
33648
|
const close = (e) => {
|
|
33448
|
-
if (!e.target.closest?.(".dshl-menu, .dshl-menubtn")) setMenu(null);
|
|
33649
|
+
if (!e.target.closest?.(".dshl-menu, .dshl-menubtn, .dshl-options")) setMenu(null);
|
|
33449
33650
|
};
|
|
33450
33651
|
window.addEventListener("mousedown", close);
|
|
33451
33652
|
return () => window.removeEventListener("mousedown", close);
|
|
@@ -33490,7 +33691,6 @@ const Player = (0, react.forwardRef)(function Player(props, ref) {
|
|
|
33490
33691
|
const h = hls.current, el = video.current;
|
|
33491
33692
|
if (h && el) {
|
|
33492
33693
|
h.config.liveSyncDuration = v;
|
|
33493
|
-
h.config.liveMaxLatencyDuration = v + 60;
|
|
33494
33694
|
const pos = h.liveSyncPosition;
|
|
33495
33695
|
if (pos != null) el.currentTime = pos;
|
|
33496
33696
|
}
|
|
@@ -33715,6 +33915,18 @@ const Player = (0, react.forwardRef)(function Player(props, ref) {
|
|
|
33715
33915
|
className: "dshl-overlay",
|
|
33716
33916
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", { className: "dshl-spinner" }), waitingText]
|
|
33717
33917
|
}),
|
|
33918
|
+
ready && held && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
33919
|
+
className: "dshl-holdnote",
|
|
33920
|
+
children: [
|
|
33921
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("i", { className: "dshl-spinner sm" }),
|
|
33922
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: held.text }),
|
|
33923
|
+
held.skip && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
33924
|
+
className: "dshl-link",
|
|
33925
|
+
onClick: () => held.skip?.(),
|
|
33926
|
+
children: t("hold.skip")
|
|
33927
|
+
})
|
|
33928
|
+
]
|
|
33929
|
+
}),
|
|
33718
33930
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)(SubtitleOverlay, {
|
|
33719
33931
|
texts,
|
|
33720
33932
|
style,
|
|
@@ -34107,17 +34319,17 @@ function RestreamPanel(props) {
|
|
|
34107
34319
|
}),
|
|
34108
34320
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("label", {
|
|
34109
34321
|
className: "rs-field",
|
|
34110
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("rs.platform") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
34322
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("rs.platform") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pick, {
|
|
34111
34323
|
value: editing.platform,
|
|
34112
|
-
onChange: (
|
|
34324
|
+
onChange: (v) => setEditing({
|
|
34113
34325
|
...editing,
|
|
34114
|
-
platform:
|
|
34115
|
-
server: FIXED[
|
|
34326
|
+
platform: v,
|
|
34327
|
+
server: FIXED[v] ?? (FIXED[editing.platform] === editing.server ? "" : editing.server)
|
|
34116
34328
|
}),
|
|
34117
|
-
|
|
34329
|
+
options: PLATFORMS.map((p) => ({
|
|
34118
34330
|
value: p,
|
|
34119
|
-
|
|
34120
|
-
}
|
|
34331
|
+
label: pname(p)
|
|
34332
|
+
}))
|
|
34121
34333
|
})]
|
|
34122
34334
|
}),
|
|
34123
34335
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
@@ -34319,7 +34531,7 @@ function Settings(props) {
|
|
|
34319
34531
|
(0, react.useEffect)(() => {
|
|
34320
34532
|
if (!open) return;
|
|
34321
34533
|
const close = (e) => {
|
|
34322
|
-
if (!box.current?.contains(e.target)) onOpen(false);
|
|
34534
|
+
if (!box.current?.contains(e.target) && !e.target.closest?.(".dshl-options")) onOpen(false);
|
|
34323
34535
|
};
|
|
34324
34536
|
window.addEventListener("mousedown", close);
|
|
34325
34537
|
return () => window.removeEventListener("mousedown", close);
|
|
@@ -34394,14 +34606,15 @@ function Settings(props) {
|
|
|
34394
34606
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
34395
34607
|
title: t("set.engine.tip"),
|
|
34396
34608
|
children: t("set.engine")
|
|
34397
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(
|
|
34609
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pick, {
|
|
34398
34610
|
value: engine,
|
|
34399
34611
|
disabled,
|
|
34400
|
-
onChange:
|
|
34401
|
-
|
|
34612
|
+
onChange: onEngine,
|
|
34613
|
+
options: providers.map((p) => ({
|
|
34402
34614
|
value: p.id,
|
|
34403
|
-
|
|
34404
|
-
|
|
34615
|
+
label: name(p.id),
|
|
34616
|
+
hint: p.ready ? void 0 : t(p.online ? "set.engine.noKey" : "set.engine.notInstalled")
|
|
34617
|
+
}))
|
|
34405
34618
|
})]
|
|
34406
34619
|
}),
|
|
34407
34620
|
cur?.online && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
@@ -34475,19 +34688,18 @@ function Settings(props) {
|
|
|
34475
34688
|
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
34476
34689
|
title: t("set.model.tip"),
|
|
34477
34690
|
children: t("set.model")
|
|
34478
|
-
}), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
34691
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pick, {
|
|
34479
34692
|
value: models?.chosen ? modelKey(models.chosen) : "",
|
|
34480
|
-
onChange:
|
|
34481
|
-
|
|
34693
|
+
onChange: chooseModel,
|
|
34694
|
+
options: [{
|
|
34482
34695
|
value: "",
|
|
34483
|
-
|
|
34484
|
-
}
|
|
34485
|
-
|
|
34486
|
-
|
|
34487
|
-
|
|
34488
|
-
|
|
34489
|
-
|
|
34490
|
-
}, pn))]
|
|
34696
|
+
label: models?.fallback ? t("set.model.default").replace("{m}", modelName(models.fallback)) : t("set.model.dsh")
|
|
34697
|
+
}, ...providersOf.flatMap((pn) => models.models.filter((m) => m.providerName === pn).map((m) => ({
|
|
34698
|
+
value: modelKey(m),
|
|
34699
|
+
label: m.name,
|
|
34700
|
+
hint: m.image ? t("set.model.image") : void 0,
|
|
34701
|
+
group: pn
|
|
34702
|
+
})))]
|
|
34491
34703
|
})]
|
|
34492
34704
|
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
34493
34705
|
className: "hint",
|
|
@@ -34495,56 +34707,61 @@ function Settings(props) {
|
|
|
34495
34707
|
})]
|
|
34496
34708
|
}),
|
|
34497
34709
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
34498
|
-
className: "srow",
|
|
34499
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.
|
|
34500
|
-
className: "
|
|
34501
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("
|
|
34502
|
-
className:
|
|
34503
|
-
|
|
34504
|
-
|
|
34505
|
-
|
|
34506
|
-
|
|
34507
|
-
|
|
34508
|
-
|
|
34509
|
-
|
|
34510
|
-
|
|
34710
|
+
className: "srow col",
|
|
34711
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
|
|
34712
|
+
className: "pair",
|
|
34713
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("set.mode") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
34714
|
+
className: "dshl-seg",
|
|
34715
|
+
children: [
|
|
34716
|
+
"site",
|
|
34717
|
+
"realtime",
|
|
34718
|
+
"polished"
|
|
34719
|
+
].map((m) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
34720
|
+
className: mode === m ? "on" : "",
|
|
34721
|
+
title: t(`mode.${m}.tip`),
|
|
34722
|
+
onClick: () => onMode(m),
|
|
34723
|
+
children: t(`mode.${m}`)
|
|
34724
|
+
}, m))
|
|
34511
34725
|
})]
|
|
34726
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
34727
|
+
className: "hint",
|
|
34728
|
+
children: t(`mode.${mode}.tip`)
|
|
34512
34729
|
})]
|
|
34513
34730
|
}),
|
|
34514
34731
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
34515
34732
|
className: "srow",
|
|
34516
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("set.spoken") }), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
34733
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("set.spoken") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pick, {
|
|
34517
34734
|
value: spoken,
|
|
34518
34735
|
disabled,
|
|
34519
|
-
onChange:
|
|
34520
|
-
|
|
34736
|
+
onChange: onSpoken,
|
|
34737
|
+
options: [{
|
|
34521
34738
|
value: "auto",
|
|
34522
|
-
|
|
34523
|
-
}
|
|
34739
|
+
label: t("set.auto")
|
|
34740
|
+
}, ...langOptions.map(([k, v]) => ({
|
|
34524
34741
|
value: k,
|
|
34525
|
-
|
|
34526
|
-
}
|
|
34742
|
+
label: v
|
|
34743
|
+
}))]
|
|
34527
34744
|
})]
|
|
34528
34745
|
}),
|
|
34529
34746
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
34530
34747
|
className: "srow",
|
|
34531
34748
|
title: t("set.login.tip"),
|
|
34532
|
-
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("set.login") }), /* @__PURE__ */ (0, react_jsx_runtime.
|
|
34749
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("set.login") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pick, {
|
|
34533
34750
|
value: login,
|
|
34534
|
-
onChange:
|
|
34535
|
-
|
|
34536
|
-
|
|
34751
|
+
onChange: onLogin,
|
|
34752
|
+
options: [
|
|
34753
|
+
{
|
|
34537
34754
|
value: "auto",
|
|
34538
|
-
|
|
34539
|
-
}
|
|
34540
|
-
browsers.map((b) =>
|
|
34755
|
+
label: t("set.auto")
|
|
34756
|
+
},
|
|
34757
|
+
...browsers.map((b) => ({
|
|
34541
34758
|
value: b.id,
|
|
34542
|
-
|
|
34543
|
-
}
|
|
34544
|
-
|
|
34759
|
+
label: b.label
|
|
34760
|
+
})),
|
|
34761
|
+
{
|
|
34545
34762
|
value: "none",
|
|
34546
|
-
|
|
34547
|
-
}
|
|
34763
|
+
label: t("set.login.none")
|
|
34764
|
+
}
|
|
34548
34765
|
]
|
|
34549
34766
|
})]
|
|
34550
34767
|
})
|
|
@@ -34649,7 +34866,7 @@ function LiveView(props) {
|
|
|
34649
34866
|
setLinesRaw(l);
|
|
34650
34867
|
store.set("lines", l);
|
|
34651
34868
|
};
|
|
34652
|
-
const [mode, setModeRaw] = (0, react.useState)(() => store.get("mode", "
|
|
34869
|
+
const [mode, setModeRaw] = (0, react.useState)(() => store.get("mode", "polished"));
|
|
34653
34870
|
const setMode = (m) => {
|
|
34654
34871
|
setModeRaw(m);
|
|
34655
34872
|
store.set("mode", m);
|
|
@@ -34702,6 +34919,13 @@ function LiveView(props) {
|
|
|
34702
34919
|
t,
|
|
34703
34920
|
ui
|
|
34704
34921
|
]);
|
|
34922
|
+
const siteBest = (0, react.useMemo)(() => {
|
|
34923
|
+
const list = job?.site ?? [];
|
|
34924
|
+
const base = (l) => l.replace(/-orig$/, "").split("-")[0];
|
|
34925
|
+
return (list.find((x) => !x.auto && base(x.lang) === ui) ?? list.find((x) => !x.auto) ?? list.find((x) => base(x.lang) === ui) ?? list[0])?.key;
|
|
34926
|
+
}, [job?.site, ui]);
|
|
34927
|
+
const siteOnly = mode === "site" && !!job;
|
|
34928
|
+
const screenLines = siteOnly && siteBest ? lines.some((l) => l.startsWith("site:")) ? lines.filter((l) => l.startsWith("site:")) : [siteBest] : lines;
|
|
34705
34929
|
const shownLines = lines;
|
|
34706
34930
|
/** The chosen recognition, or — when it can't run here (no key, not installed) — the first one that can. */
|
|
34707
34931
|
const usableEngine = () => {
|
|
@@ -34738,8 +34962,8 @@ function LiveView(props) {
|
|
|
34738
34962
|
}).catch(() => {});
|
|
34739
34963
|
}, [sessionId]);
|
|
34740
34964
|
(0, react.useEffect)(() => {
|
|
34741
|
-
setSubStyle((s) => reconcile(s,
|
|
34742
|
-
}, [
|
|
34965
|
+
setSubStyle((s) => reconcile(s, screenLines));
|
|
34966
|
+
}, [screenLines.join(",")]);
|
|
34743
34967
|
(0, react.useEffect)(() => {
|
|
34744
34968
|
saveStyle(subStyle);
|
|
34745
34969
|
}, [subStyle]);
|
|
@@ -34838,6 +35062,7 @@ function LiveView(props) {
|
|
|
34838
35062
|
url: target
|
|
34839
35063
|
} : classify(target);
|
|
34840
35064
|
if (!c) return;
|
|
35065
|
+
if (props.onNewVideo && job && job.source !== "push" && job.url !== c.url && c.source !== "push" && await props.onNewVideo(target)) return;
|
|
34841
35066
|
try {
|
|
34842
35067
|
const j = await post("start", {
|
|
34843
35068
|
...options(),
|
|
@@ -34862,7 +35087,60 @@ function LiveView(props) {
|
|
|
34862
35087
|
state: "off",
|
|
34863
35088
|
at: 0
|
|
34864
35089
|
};
|
|
34865
|
-
const
|
|
35090
|
+
const [noWait, setNoWait] = (0, react.useState)(false);
|
|
35091
|
+
(0, react.useEffect)(() => {
|
|
35092
|
+
setNoWait(false);
|
|
35093
|
+
}, [mode, job?.id]);
|
|
35094
|
+
const lastPolished = (0, react.useMemo)(() => cues.reduce((m, c) => c.polished && c.end > m ? c.end : m, 0), [cues]);
|
|
35095
|
+
(0, react.useEffect)(() => {
|
|
35096
|
+
const p = player.current;
|
|
35097
|
+
if (!p) return;
|
|
35098
|
+
const waiting = !!job && job.status === "running" && !job.vod && mode !== "site" && subs.state === "running" && !noWait;
|
|
35099
|
+
const ready = mode === "polished" ? Math.max(lastPolished, subs.at - 150) : subs.at;
|
|
35100
|
+
if (waiting && now > ready + .5) p.hold(t(mode === "polished" ? "hold.polished" : "hold.realtime"), () => setNoWait(true));
|
|
35101
|
+
else if (!waiting || ready > now + (mode === "polished" ? 20 : 4)) p.hold(null);
|
|
35102
|
+
}, [
|
|
35103
|
+
now,
|
|
35104
|
+
subs.at,
|
|
35105
|
+
subs.state,
|
|
35106
|
+
lastPolished,
|
|
35107
|
+
mode,
|
|
35108
|
+
noWait,
|
|
35109
|
+
job?.status
|
|
35110
|
+
]);
|
|
35111
|
+
(0, react.useEffect)(() => {
|
|
35112
|
+
if (!job?.title || !props.onTitle) return;
|
|
35113
|
+
const key = `dsh-live.named.${sessionId}`;
|
|
35114
|
+
try {
|
|
35115
|
+
if (localStorage.getItem(key) === job.id) return;
|
|
35116
|
+
localStorage.setItem(key, job.id);
|
|
35117
|
+
} catch {}
|
|
35118
|
+
props.onTitle(job.title);
|
|
35119
|
+
}, [job?.id, job?.title]);
|
|
35120
|
+
const rateLog = (0, react.useRef)([]);
|
|
35121
|
+
const [rate, setRate] = (0, react.useState)(null);
|
|
35122
|
+
(0, react.useEffect)(() => {
|
|
35123
|
+
if (!job || job.vod || job.status !== "running" || subs.state !== "running") {
|
|
35124
|
+
rateLog.current = [];
|
|
35125
|
+
setRate(null);
|
|
35126
|
+
return;
|
|
35127
|
+
}
|
|
35128
|
+
const log = rateLog.current;
|
|
35129
|
+
log.push({
|
|
35130
|
+
wall: Date.now(),
|
|
35131
|
+
at: subs.at
|
|
35132
|
+
});
|
|
35133
|
+
while (log.length > 2 && log[0].wall < Date.now() - 12e4) log.shift();
|
|
35134
|
+
const first = log[0], last = log.at(-1);
|
|
35135
|
+
setRate(last.wall - first.wall > 45e3 ? (last.at - first.at) / ((last.wall - first.wall) / 1e3) : null);
|
|
35136
|
+
}, [
|
|
35137
|
+
subs.at,
|
|
35138
|
+
subs.state,
|
|
35139
|
+
job?.status,
|
|
35140
|
+
job?.id
|
|
35141
|
+
]);
|
|
35142
|
+
const emptyLines = subs.state === "running" && cues.length === 0 && subs.at > 15 ? t("tr.listening").replace("{t}", CLOCK(subs.at)) : t("tr.empty");
|
|
35143
|
+
const startSubs = async (from, withMode) => {
|
|
34866
35144
|
if (!job) return;
|
|
34867
35145
|
setError("");
|
|
34868
35146
|
const p = providers.find((x) => x.id === usableEngine());
|
|
@@ -34874,6 +35152,7 @@ function LiveView(props) {
|
|
|
34874
35152
|
try {
|
|
34875
35153
|
setJob(await post("subs", {
|
|
34876
35154
|
...options(),
|
|
35155
|
+
...withMode ? { mode: withMode } : {},
|
|
34877
35156
|
jobId: job.id,
|
|
34878
35157
|
action: "start",
|
|
34879
35158
|
from
|
|
@@ -34888,6 +35167,17 @@ function LiveView(props) {
|
|
|
34888
35167
|
action: "pause"
|
|
34889
35168
|
}).catch(() => job));
|
|
34890
35169
|
};
|
|
35170
|
+
/** Switching modes while a video is open takes effect right away: no delay stops transcribing, the others (re)start it. */
|
|
35171
|
+
const changeMode = (m) => {
|
|
35172
|
+
if (m === mode) return;
|
|
35173
|
+
setMode(m);
|
|
35174
|
+
if (!job || job.status === "error") return;
|
|
35175
|
+
if (m === "site") {
|
|
35176
|
+
if (subs.state === "running") pauseSubs();
|
|
35177
|
+
return;
|
|
35178
|
+
}
|
|
35179
|
+
startSubs(subs.state === "off" ? now : void 0, m);
|
|
35180
|
+
};
|
|
34891
35181
|
const changeLines = async (next) => {
|
|
34892
35182
|
const before = translationsOf(lines).join(","), after = translationsOf(next).join(",");
|
|
34893
35183
|
setLines(next);
|
|
@@ -34901,6 +35191,16 @@ function LiveView(props) {
|
|
|
34901
35191
|
const stop = async () => {
|
|
34902
35192
|
if (job) await post("stop", { jobId: job.id }).catch(() => {});
|
|
34903
35193
|
};
|
|
35194
|
+
/** Leave this video: everything for it stops, the page is empty again (its files stay in the video folder). */
|
|
35195
|
+
const closeVideo = async () => {
|
|
35196
|
+
if (!job) return;
|
|
35197
|
+
await post("close", { jobId: job.id }).catch(() => {});
|
|
35198
|
+
setJob(null);
|
|
35199
|
+
setCues([]);
|
|
35200
|
+
setTerms([]);
|
|
35201
|
+
setInput("");
|
|
35202
|
+
setBench(null);
|
|
35203
|
+
};
|
|
34904
35204
|
const onFile = async (file) => {
|
|
34905
35205
|
setError("");
|
|
34906
35206
|
setUploading(0);
|
|
@@ -34932,7 +35232,7 @@ function LiveView(props) {
|
|
|
34932
35232
|
const translatable = langOptions.filter(([k]) => k !== spokenBase);
|
|
34933
35233
|
const nameWithSpoken = (k) => k === "src" && spokenBase ? `${t("sub.source")} · ${langName(spokenBase)}` : langName(k);
|
|
34934
35234
|
const [siteCues, setSiteCues] = (0, react.useState)({});
|
|
34935
|
-
const siteKeys =
|
|
35235
|
+
const siteKeys = screenLines.filter((l) => l.startsWith("site:") && job?.site?.some((s) => s.key === l));
|
|
34936
35236
|
(0, react.useEffect)(() => {
|
|
34937
35237
|
if (!job) return;
|
|
34938
35238
|
for (const k of siteKeys) {
|
|
@@ -35231,7 +35531,7 @@ function LiveView(props) {
|
|
|
35231
35531
|
},
|
|
35232
35532
|
children: [shown.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
35233
35533
|
className: "dshl-muted pad",
|
|
35234
|
-
children:
|
|
35534
|
+
children: emptyLines
|
|
35235
35535
|
}), shown.map((c) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
35236
35536
|
className: `dshl-line ${c.id === activeId ? "active" : ""} ${c.polished ? "ok" : "raw"}`,
|
|
35237
35537
|
onClick: () => player.current?.seek(c.start),
|
|
@@ -35263,7 +35563,7 @@ function LiveView(props) {
|
|
|
35263
35563
|
children: [
|
|
35264
35564
|
paragraphs.length === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
35265
35565
|
className: "dshl-muted pad",
|
|
35266
|
-
children:
|
|
35566
|
+
children: emptyLines
|
|
35267
35567
|
}),
|
|
35268
35568
|
paragraphs.some((p) => p.speaker) && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
|
|
35269
35569
|
className: "dshl-muted small",
|
|
@@ -35301,6 +35601,23 @@ function LiveView(props) {
|
|
|
35301
35601
|
onTerms: setTerms
|
|
35302
35602
|
})
|
|
35303
35603
|
] });
|
|
35604
|
+
const slow = !!job && job.status === "running" && !job.vod && mode !== "site" && rate !== null && rate < .95;
|
|
35605
|
+
const engineNow = providers.find((x) => x.id === usableEngine());
|
|
35606
|
+
const liveNote = siteOnly && job && job.status !== "resolving" && !job.site?.length ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
35607
|
+
className: "dshl-lag",
|
|
35608
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t("site.none") }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
35609
|
+
className: "dshl-link",
|
|
35610
|
+
onClick: () => changeMode("polished"),
|
|
35611
|
+
children: t("site.switch")
|
|
35612
|
+
})]
|
|
35613
|
+
}) : slow ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
35614
|
+
className: "dshl-lag warn",
|
|
35615
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", { children: t(engineNow && !engineNow.online ? "lag.slowLocal" : "lag.slow").replace("{x}", rate.toFixed(2)) }), engineNow && !engineNow.online && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
35616
|
+
className: "dshl-link",
|
|
35617
|
+
onClick: () => setSettingsOpen(true),
|
|
35618
|
+
children: t("lag.settings")
|
|
35619
|
+
})]
|
|
35620
|
+
}) : null;
|
|
35304
35621
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
35305
35622
|
className: `dshl ${theater && !activeBench ? "theater" : ""} ${pageMode ? "page" : ""} ${activeBench ? "bench" : ""}`,
|
|
35306
35623
|
"data-dsh-own-drop": "dsh-vidi",
|
|
@@ -35383,7 +35700,7 @@ function LiveView(props) {
|
|
|
35383
35700
|
engine: engine || "local",
|
|
35384
35701
|
onEngine: setEngine,
|
|
35385
35702
|
mode,
|
|
35386
|
-
onMode:
|
|
35703
|
+
onMode: changeMode,
|
|
35387
35704
|
spoken,
|
|
35388
35705
|
onSpoken: setSpoken,
|
|
35389
35706
|
langOptions,
|
|
@@ -35456,7 +35773,7 @@ function LiveView(props) {
|
|
|
35456
35773
|
siteCues: shownSite,
|
|
35457
35774
|
spoken: spokenBase,
|
|
35458
35775
|
style: subStyle,
|
|
35459
|
-
delay: job.mode === "polished"
|
|
35776
|
+
delay: job.vod ? delay : mode === "polished" ? Math.max(delay, 75) : mode === "site" ? 3 : delay,
|
|
35460
35777
|
onDelay: setDelay,
|
|
35461
35778
|
showDelay: job.status === "running" && !job.vod,
|
|
35462
35779
|
onSnapshot: saveSnapshot,
|
|
@@ -35567,9 +35884,17 @@ function LiveView(props) {
|
|
|
35567
35884
|
className: "dshl-btn small",
|
|
35568
35885
|
onClick: stop,
|
|
35569
35886
|
children: t("in.stop")
|
|
35887
|
+
}),
|
|
35888
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
35889
|
+
className: "dshl-btn small icon quiet",
|
|
35890
|
+
title: t("in.close"),
|
|
35891
|
+
"aria-label": t("in.close"),
|
|
35892
|
+
onClick: () => void closeVideo(),
|
|
35893
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Icon.close, {})
|
|
35570
35894
|
})
|
|
35571
35895
|
]
|
|
35572
35896
|
}),
|
|
35897
|
+
liveNote,
|
|
35573
35898
|
pageMode && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("section", {
|
|
35574
35899
|
className: "dshl-under",
|
|
35575
35900
|
children: transcriptBox
|
|
@@ -35669,7 +35994,7 @@ function SubsStarter(props) {
|
|
|
35669
35994
|
(0, react.useEffect)(() => {
|
|
35670
35995
|
if (!open) return;
|
|
35671
35996
|
const close = (e) => {
|
|
35672
|
-
if (!box.current?.contains(e.target) && !pop.current?.contains(e.target)) setOpen(false);
|
|
35997
|
+
if (!box.current?.contains(e.target) && !pop.current?.contains(e.target) && !e.target.closest?.(".dshl-options")) setOpen(false);
|
|
35673
35998
|
};
|
|
35674
35999
|
window.addEventListener("mousedown", close);
|
|
35675
36000
|
return () => window.removeEventListener("mousedown", close);
|
|
@@ -36225,8 +36550,18 @@ const en = {
|
|
|
36225
36550
|
"in.remove": "Remove",
|
|
36226
36551
|
"mode.realtime": "Real-time",
|
|
36227
36552
|
"mode.polished": "Polished",
|
|
36228
|
-
"mode.
|
|
36229
|
-
"mode.
|
|
36553
|
+
"mode.site": "No delay",
|
|
36554
|
+
"mode.site.tip": "The site’s own subtitles only: nothing is transcribed or translated, the picture is not held back",
|
|
36555
|
+
"mode.realtime.tip": "A few seconds behind: transcribed and translated as it is heard",
|
|
36556
|
+
"mode.polished.tip": "1–2 minutes behind: misheard words fixed with context and the picture — the most accurate (default)",
|
|
36557
|
+
"site.none": "This video has no subtitles of its own.",
|
|
36558
|
+
"site.switch": "Switch to Polished",
|
|
36559
|
+
"hold.realtime": "Waiting for subtitles…",
|
|
36560
|
+
"hold.polished": "Waiting for polished subtitles…",
|
|
36561
|
+
"hold.skip": "Don’t wait",
|
|
36562
|
+
"lag.slow": "Transcription runs slower than the stream ({x}×), so subtitles fall further behind.",
|
|
36563
|
+
"lag.slowLocal": "This computer transcribes slower than the stream ({x}×), so subtitles fall further behind. An online service keeps up.",
|
|
36564
|
+
"lag.settings": "Choose one",
|
|
36230
36565
|
"in.uploading": "Uploading {p}%",
|
|
36231
36566
|
"in.drop": "Drop to start",
|
|
36232
36567
|
"empty.title": "Subtitles for any video",
|
|
@@ -36363,6 +36698,8 @@ const en = {
|
|
|
36363
36698
|
"tr.copyShort": "Copy",
|
|
36364
36699
|
"tr.copied": "Copied",
|
|
36365
36700
|
"tr.empty": "Subtitles show up here.",
|
|
36701
|
+
"in.close": "Close this video (its files stay in the video folder)",
|
|
36702
|
+
"tr.listening": "Listening — no speech yet (music or a break so far). Heard up to {t}.",
|
|
36366
36703
|
"gl.new": "New term",
|
|
36367
36704
|
"gl.add": "Add",
|
|
36368
36705
|
"gl.auto": "auto",
|
|
@@ -36549,8 +36886,18 @@ const zh = {
|
|
|
36549
36886
|
"in.remove": "移除",
|
|
36550
36887
|
"mode.realtime": "实时",
|
|
36551
36888
|
"mode.polished": "精修",
|
|
36552
|
-
"mode.
|
|
36553
|
-
"mode.
|
|
36889
|
+
"mode.site": "无延迟",
|
|
36890
|
+
"mode.site.tip": "只看网站自带的字幕:不转写、不翻译,画面也不往后拖",
|
|
36891
|
+
"mode.realtime.tip": "晚几秒:听到就转写、翻译",
|
|
36892
|
+
"mode.polished.tip": "晚 1–2 分钟:结合上下文和画面改正听错的词,字幕最准(默认)",
|
|
36893
|
+
"site.none": "这个视频没有网站自带的字幕。",
|
|
36894
|
+
"site.switch": "换成精修",
|
|
36895
|
+
"hold.realtime": "等字幕…",
|
|
36896
|
+
"hold.polished": "等精修字幕…",
|
|
36897
|
+
"hold.skip": "不等了",
|
|
36898
|
+
"lag.slow": "转写比直播慢({x} 倍速),字幕会越落越远。",
|
|
36899
|
+
"lag.slowLocal": "本机转写比直播慢({x} 倍速),字幕会越落越远。换成在线转写就能跟上。",
|
|
36900
|
+
"lag.settings": "去选一个",
|
|
36554
36901
|
"in.uploading": "上传中 {p}%",
|
|
36555
36902
|
"in.drop": "松手开始",
|
|
36556
36903
|
"empty.title": "给任何视频配字幕",
|
|
@@ -36687,6 +37034,8 @@ const zh = {
|
|
|
36687
37034
|
"tr.copyShort": "复制",
|
|
36688
37035
|
"tr.copied": "已复制",
|
|
36689
37036
|
"tr.empty": "字幕会出现在这里",
|
|
37037
|
+
"in.close": "关闭这个视频(文件还留在视频目录里)",
|
|
37038
|
+
"tr.listening": "正在听,还没有人说话(目前是音乐或插播)。已听到 {t}。",
|
|
36690
37039
|
"gl.new": "新词条",
|
|
36691
37040
|
"gl.add": "添加",
|
|
36692
37041
|
"gl.auto": "自动",
|
|
@@ -36820,6 +37169,7 @@ const CSS = `
|
|
|
36820
37169
|
.dshl-btn.primary:hover:not(:disabled) { background: var(--accent); filter: brightness(1.06); }
|
|
36821
37170
|
.dshl-btn.stop { background: var(--live); border-color: var(--live); color: #fff; }
|
|
36822
37171
|
.dshl-btn.icon { width: 40px; padding: 0; }
|
|
37172
|
+
.dshl-btn.small.icon { width: 30px; }
|
|
36823
37173
|
.dshl-btn.icon.back { color: var(--muted); }
|
|
36824
37174
|
.dshl-btn.icon.back svg { transform: rotate(180deg); }
|
|
36825
37175
|
.dshl-btn.block { display: flex; width: 100%; }
|
|
@@ -36847,10 +37197,25 @@ const CSS = `
|
|
|
36847
37197
|
.dshl-switch.on { background: var(--accent); }
|
|
36848
37198
|
.dshl-switch.on i { left: 16px; }
|
|
36849
37199
|
|
|
36850
|
-
.dshl
|
|
36851
|
-
|
|
36852
|
-
|
|
36853
|
-
.dshl
|
|
37200
|
+
.dshl-select { display: inline-flex; align-items: center; gap: 8px; min-width: 0; max-width: 100%; height: 32px; padding: 0 10px; border: 1px solid var(--line); border-radius: 8px;
|
|
37201
|
+
background: var(--bg); color: var(--ink); font: inherit; font-size: 13px; text-align: left; cursor: pointer; }
|
|
37202
|
+
.dshl-select .v { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
37203
|
+
.dshl-select .caret { flex: none; width: 6px; height: 6px; margin: -3px 1px 0 0; border-right: 1.5px solid var(--faint); border-bottom: 1.5px solid var(--faint); transform: rotate(45deg); transition: transform .15s; }
|
|
37204
|
+
.dshl-select.open .caret { margin-top: 3px; transform: rotate(-135deg); }
|
|
37205
|
+
.dshl-select:hover:not(:disabled), .dshl-select.open { border-color: color-mix(in srgb, var(--ink) 25%, var(--line)); }
|
|
37206
|
+
.dshl-select:focus-visible { outline: 2px solid color-mix(in srgb, var(--accent) 45%, transparent); outline-offset: 1px; }
|
|
37207
|
+
.dshl-select:disabled { opacity: .5; cursor: default; }
|
|
37208
|
+
/* The list: drawn at the page's top level (so no panel clips it), in the colours of where it opened. */
|
|
37209
|
+
.dshl-options { position: fixed; z-index: 2147483000; overflow: auto; overscroll-behavior: contain; padding: 4px; border: 1px solid var(--line, #e4e3de); border-radius: 10px;
|
|
37210
|
+
background: var(--bg, #fff); color: var(--ink, #1b1b19); box-shadow: 0 10px 30px rgba(0,0,0,.18), 0 2px 6px rgba(0,0,0,.08); font-size: 13px; line-height: 1.4; -webkit-font-smoothing: antialiased; }
|
|
37211
|
+
.dshl-options .grp { padding: 8px 10px 4px; color: var(--faint, #9a9993); font-size: 11.5px; font-weight: 600; }
|
|
37212
|
+
.dshl-options .opt { display: flex; align-items: center; gap: 8px; min-height: 32px; padding: 6px 10px; border-radius: 6px; cursor: pointer; }
|
|
37213
|
+
.dshl-options .opt.act { background: var(--hover, rgba(0,0,0,.05)); }
|
|
37214
|
+
.dshl-options .opt.on { font-weight: 550; }
|
|
37215
|
+
.dshl-options .opt.off { opacity: .45; cursor: default; }
|
|
37216
|
+
.dshl-options .opt .l { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
37217
|
+
.dshl-options .opt .h { flex: none; color: var(--faint, #9a9993); font-size: 12px; }
|
|
37218
|
+
.dshl-options .opt .ok { flex: none; margin-left: auto; color: var(--accent, #3b6ef5); }
|
|
36854
37219
|
|
|
36855
37220
|
/* Toggle chips (several can be on): languages to show or download */
|
|
36856
37221
|
.dshl-chips .row, .dshl-checks2 { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
@@ -36861,7 +37226,8 @@ const CSS = `
|
|
|
36861
37226
|
.dshl-chips .chip i { display: inline-flex; align-items: center; justify-content: center; width: 16px; height: 16px; margin-left: -3px; border-radius: 50%;
|
|
36862
37227
|
background: var(--accent); color: #fff; font-size: 10.5px; font-style: normal; font-weight: 600; }
|
|
36863
37228
|
.dshl-chips .chip.site:not(.on) { border-style: dashed; }
|
|
36864
|
-
.dshl-chips select.chip.add {
|
|
37229
|
+
.dshl-chips .dshl-select.chip.add { height: 28px; border-style: dashed; padding: 0 10px; background: transparent; }
|
|
37230
|
+
.dshl-chips .dshl-select.chip.add .caret { display: none; }
|
|
36865
37231
|
.dshl-chips .chip.ghost { width: 28px; padding: 0; justify-content: center; }
|
|
36866
37232
|
.dshl-checks2 { margin: 0 0 10px; }
|
|
36867
37233
|
.dshl-checks2 button i { width: 14px; height: 14px; margin-left: -2px; border: 1.5px solid currentColor; border-radius: 4px; opacity: .55; flex: none; }
|
|
@@ -36945,7 +37311,7 @@ html:not([data-platform]) [data-sidebar-drawer] .dshl-panelwrap .dshl.page .dshl
|
|
|
36945
37311
|
every state, full screen included; the bar's 40px row is centred on the same line. */
|
|
36946
37312
|
html[data-platform='darwin'] .dshl-panelwrap .dshl.page .dshl-top { padding-top: 5px; padding-bottom: 8px; }
|
|
36947
37313
|
html[data-platform='darwin'] .dshl.page .dshl-top { -webkit-app-region: drag; app-region: drag; }
|
|
36948
|
-
html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, select, .dshl-pick) { -webkit-app-region: no-drag; app-region: no-drag; }
|
|
37314
|
+
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; }
|
|
36949
37315
|
.dshl.page { display: flex; flex-direction: column; height: 100%; overflow: hidden; }
|
|
36950
37316
|
.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; }
|
|
36951
37317
|
/* Video page: player on top, then the status row, then subtitles/glossary filling what is left; chat on the right. */
|
|
@@ -36955,6 +37321,8 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, s
|
|
|
36955
37321
|
.dshl.page .dshl-side { position: static; height: 100%; max-height: none; }
|
|
36956
37322
|
/* Workbench open: the video and its subtitles keep a column on the left; the bench takes the rest. */
|
|
36957
37323
|
.dshl.page.bench .dshl-main { grid-template-columns: clamp(340px, 32vw, 520px) minmax(0, 1fr); }
|
|
37324
|
+
.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; }
|
|
37325
|
+
.dshl-lag.warn { background: color-mix(in srgb, #f5a524 14%, transparent); color: var(--ink); }
|
|
36958
37326
|
.dshl-guard { display: grid; gap: 8px; margin: 12px; padding: 12px 14px; border: 1px solid color-mix(in srgb, #d1434b 35%, var(--line)); border-radius: 10px; color: var(--ink); font-size: 13px; }
|
|
36959
37327
|
.dshl-guard p { margin: 0; line-height: 1.6; }
|
|
36960
37328
|
.dshl-guard code { color: var(--muted); font-size: 12px; white-space: pre-wrap; word-break: break-word; }
|
|
@@ -37004,6 +37372,9 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, s
|
|
|
37004
37372
|
.dshl-stage.pagefull { position: fixed; inset: 0; z-index: 2147483000; width: 100vw; height: 100vh; aspect-ratio: auto; border-radius: 0; }
|
|
37005
37373
|
.dshl-stage video { display: block; width: 100%; height: 100%; object-fit: contain; background: #000; }
|
|
37006
37374
|
.dshl-stage.idle { cursor: none; }
|
|
37375
|
+
.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%);
|
|
37376
|
+
background: rgba(20,20,22,.78); color: #f2f2f3; font-size: 12.5px; white-space: nowrap; backdrop-filter: blur(8px); }
|
|
37377
|
+
.dshl-holdnote .dshl-link { color: #9ec0ff; }
|
|
37007
37378
|
.dshl-overlay { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 14px; color: #cfcfcf; font-size: 14px; }
|
|
37008
37379
|
.dshl-spinner { width: 28px; height: 28px; border: 2.5px solid rgba(255,255,255,.18); border-top-color: #fff; border-radius: 50%; animation: dshl-spin .9s linear infinite; }
|
|
37009
37380
|
.dshl-spinner.sm { width: 14px; height: 14px; border-width: 2px; flex: none; }
|
|
@@ -37129,11 +37500,7 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, s
|
|
|
37129
37500
|
.dshl-submenu { display: grid; gap: 4px; }
|
|
37130
37501
|
.dshl-submenu .slots { display: grid; gap: 8px; padding: 4px 6px 10px; }
|
|
37131
37502
|
.dshl-submenu .slot { display: grid; grid-template-columns: 56px 1fr; align-items: center; gap: 10px; color: var(--muted); }
|
|
37132
|
-
.dshl-submenu .slot select { width: 100%;
|
|
37133
|
-
appearance: none; -webkit-appearance: none;
|
|
37134
|
-
background: var(--l1) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M1 1l4 4 4-4' fill='none' stroke='%23b4b4ba' stroke-width='1.5'/%3E%3C/svg%3E") no-repeat right 10px center; }
|
|
37135
|
-
.dshl-submenu .slot select:hover { border-color: rgba(255,255,255,.28); }
|
|
37136
|
-
.dshl-submenu .slot select option, .dshl-submenu .slot select optgroup { background: #fff; color: #111; }
|
|
37503
|
+
.dshl-submenu .slot .dshl-select { width: 100%; height: 34px; background: var(--l1); }
|
|
37137
37504
|
.dshl-submenu .swap { justify-self: start; height: 26px; margin-left: 66px; padding: 0 10px; border: 0; border-radius: 6px; background: var(--l2); color: var(--muted); font-size: 12px; cursor: pointer; }
|
|
37138
37505
|
.dshl-submenu .swap:hover { color: var(--ink); }
|
|
37139
37506
|
.dshl-submenu .opts { position: sticky; bottom: -10px; display: grid; gap: 2px; margin: 0 -8px -10px; padding: 6px 8px 10px; border-top: 1px solid var(--line-soft); background: rgb(28,28,31); }
|
|
@@ -37173,12 +37540,12 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, s
|
|
|
37173
37540
|
.dshl-rs .rs-hint { margin: 0; color: var(--faint); font-size: 12px; line-height: 1.55; }
|
|
37174
37541
|
.dshl-rs .rs-err { margin: 0; color: #ff8f92; font-size: 12px; line-height: 1.55; }
|
|
37175
37542
|
.dshl-rs .rs-field { display: grid; grid-template-columns: 76px 1fr; align-items: center; gap: 10px; font-size: 13px; color: var(--muted); }
|
|
37176
|
-
.dshl-rs .rs-field
|
|
37177
|
-
.dshl-rs .rs-field
|
|
37178
|
-
.dshl-rs .rs-field input:focus
|
|
37543
|
+
.dshl-rs .rs-field .dshl-select { width: 100%; height: 34px; background: var(--l1); }
|
|
37544
|
+
.dshl-rs .rs-field input { width: 100%; height: 34px; padding: 0 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--l1); color: var(--ink); font: inherit; font-size: 13px; }
|
|
37545
|
+
.dshl-rs .rs-field input:focus { outline: none; border-color: var(--accent); }
|
|
37179
37546
|
.dshl-rs .rs-actions { display: flex; justify-content: flex-end; gap: 8px; }
|
|
37180
37547
|
.dshl-menu.sheet .dshl-rs .rs-row { min-height: 50px; }
|
|
37181
|
-
.dshl-menu.sheet .dshl-rs .rs-field input, .dshl-menu.sheet .dshl-rs .rs-field select { height: 42px; font-size: 15px; }
|
|
37548
|
+
.dshl-menu.sheet .dshl-rs .rs-field input, .dshl-menu.sheet .dshl-rs .rs-field .dshl-select { height: 42px; font-size: 15px; }
|
|
37182
37549
|
.dshl-menu.sheet .dshl-rs .dshl-btn.block { height: 46px; font-size: 15px; }
|
|
37183
37550
|
.dshl-dl { display: grid; gap: 14px; padding: 0 6px; }
|
|
37184
37551
|
.dshl-dl .grp { display: grid; gap: 8px; }
|
|
@@ -37208,7 +37575,7 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, s
|
|
|
37208
37575
|
.dshl-menu.sheet .dshl-seg button, .dshl-menu.sheet .dshl-submenu .seg button { min-width: 52px; height: 34px; font-size: 14px; }
|
|
37209
37576
|
.dshl-menu.sheet .dshl-submenu .seg { width: 210px; }
|
|
37210
37577
|
.dshl-menu.sheet .dshl-submenu .slot { grid-template-columns: 64px 1fr; font-size: 15px; }
|
|
37211
|
-
.dshl-menu.sheet .dshl-submenu .slot select { height: 44px; font-size: 15px; }
|
|
37578
|
+
.dshl-menu.sheet .dshl-submenu .slot .dshl-select { height: 44px; font-size: 15px; }
|
|
37212
37579
|
.dshl-menu.sheet .dshl-submenu .swap { height: 34px; margin-left: 74px; font-size: 14px; }
|
|
37213
37580
|
.dshl-menu.sheet .dshl-switch { width: 44px; height: 26px; }
|
|
37214
37581
|
.dshl-menu.sheet .dshl-switch i { width: 22px; height: 22px; }
|
|
@@ -37328,7 +37695,7 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, s
|
|
|
37328
37695
|
.dshl .srow { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 46px; font-size: 13.5px; }
|
|
37329
37696
|
.dshl-pop.settings { width: 320px; padding: 6px 12px; }
|
|
37330
37697
|
.dshl-pop.settings .srow + .srow { border-top: 1px solid var(--line-soft); }
|
|
37331
|
-
.dshl-pop.settings .srow > select, .dshl-pop.settings .srow > .dshl-seg { width: 168px; }
|
|
37698
|
+
.dshl-pop.settings .srow > .dshl-select, .dshl-pop.settings .srow > .dshl-seg { width: 168px; }
|
|
37332
37699
|
.dshl-pop.settings .srow > .dshl-seg button { flex: 1; padding: 0 6px; }
|
|
37333
37700
|
.dshl .srow.col { display: grid; justify-content: stretch; gap: 8px; padding: 10px 0; }
|
|
37334
37701
|
.dshl .srow .keyline { display: flex; align-items: center; gap: 6px; }
|
|
@@ -37343,7 +37710,7 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, s
|
|
|
37343
37710
|
.dshl .srow .keyfoot { display: flex; align-items: baseline; justify-content: space-between; gap: 10px; color: var(--faint); font-size: 12px; line-height: 1.5; }
|
|
37344
37711
|
.dshl .srow .hint { color: var(--muted); font-size: 12.5px; line-height: 1.6; }
|
|
37345
37712
|
.dshl .srow .pair { display: flex; align-items: center; justify-content: space-between; gap: 12px; }
|
|
37346
|
-
.dshl .srow .pair > select { width: 168px; }
|
|
37713
|
+
.dshl .srow .pair > .dshl-select { width: 168px; }
|
|
37347
37714
|
.dshl-pop.usage { min-width: 320px; padding: 12px 14px; }
|
|
37348
37715
|
.dshl-pop.usage h3 { margin: 0 0 8px; color: var(--muted); font-size: 12.5px; font-weight: 600; }
|
|
37349
37716
|
.dshl-pop.usage table { width: 100%; border-collapse: collapse; font-size: 12.5px; font-variant-numeric: tabular-nums; }
|
|
@@ -37439,12 +37806,12 @@ const samePath = (a, b) => !!a && !!b && a.replace(/\/+$/, "") === b.replace(/\/
|
|
|
37439
37806
|
* It lives in a "Video" workspace on the video folder, so DeepSeek's own file
|
|
37440
37807
|
* and shell tools can work on the recordings, clips and frames directly.
|
|
37441
37808
|
*/
|
|
37442
|
-
async function pageSession(sessions, workspaces, title) {
|
|
37809
|
+
async function pageSession(sessions, workspaces, title, fresh = false) {
|
|
37443
37810
|
let saved = null;
|
|
37444
37811
|
try {
|
|
37445
37812
|
saved = localStorage.getItem(PAGE_SESSION);
|
|
37446
37813
|
} catch {}
|
|
37447
|
-
if (saved && sessions.has(saved)) return saved;
|
|
37814
|
+
if (!fresh && saved && sessions.has(saved)) return saved;
|
|
37448
37815
|
const deps = await fetch("/live/api/deps").then((r) => r.json());
|
|
37449
37816
|
if (!deps?.dataDir) throw new Error("the video plugin is not running yet");
|
|
37450
37817
|
const known = () => workspaces.list?.getSnapshot().items?.find((w) => samePath(w.path, deps.dataDir));
|
|
@@ -37508,9 +37875,48 @@ function closePage(sessions) {
|
|
|
37508
37875
|
});
|
|
37509
37876
|
if (returnTo && sessions.has(returnTo)) sessions.open(returnTo);
|
|
37510
37877
|
}
|
|
37878
|
+
/** The video folder (the "Video" workspace), asked once. */
|
|
37879
|
+
let dataDirP;
|
|
37880
|
+
const videoFolder = () => dataDirP ??= fetch("/live/api/deps").then((r) => r.json()).then((d) => d?.dataDir).catch(() => {
|
|
37881
|
+
dataDirP = void 0;
|
|
37882
|
+
});
|
|
37883
|
+
/**
|
|
37884
|
+
* Each video gets a conversation of its own, named after it ("▶ title"), so
|
|
37885
|
+
* the history lists videos rather than a row of "Untitled". This applies to
|
|
37886
|
+
* the conversations in the video folder; a chat elsewhere that opened a video
|
|
37887
|
+
* keeps its own name and switches video in place.
|
|
37888
|
+
*/
|
|
37889
|
+
function videoHandlers(sessions, workspaces, sessionId, fallbackTitle) {
|
|
37890
|
+
const own = async () => {
|
|
37891
|
+
const cwd = sessions.info?.(sessionId)?.cwd;
|
|
37892
|
+
const dir = await videoFolder();
|
|
37893
|
+
return !!cwd && !!dir && samePath(cwd, dir);
|
|
37894
|
+
};
|
|
37895
|
+
return {
|
|
37896
|
+
onNewVideo: async (link) => {
|
|
37897
|
+
if (!workspaces || !await own()) return false;
|
|
37898
|
+
try {
|
|
37899
|
+
await openPage(sessions, {
|
|
37900
|
+
sessionId: await pageSession(sessions, workspaces, fallbackTitle, true),
|
|
37901
|
+
pending: link,
|
|
37902
|
+
workspaces
|
|
37903
|
+
});
|
|
37904
|
+
return true;
|
|
37905
|
+
} catch {
|
|
37906
|
+
return false;
|
|
37907
|
+
}
|
|
37908
|
+
},
|
|
37909
|
+
onTitle: (title) => {
|
|
37910
|
+
own().then((yes) => {
|
|
37911
|
+
const now = sessions.info?.(sessionId)?.title;
|
|
37912
|
+
if (yes && sessions.rename && (!now || now.startsWith("▶ "))) sessions.rename(sessionId, `▶ ${title}`.slice(0, 80));
|
|
37913
|
+
});
|
|
37914
|
+
}
|
|
37915
|
+
};
|
|
37916
|
+
}
|
|
37511
37917
|
/** The video page as a main panel (newer DS-Harness): the app's sidebar and its toggle stay. */
|
|
37512
37918
|
function VideoPanel(props) {
|
|
37513
|
-
const { sessions, t } = props;
|
|
37919
|
+
const { sessions, workspaces, t } = props;
|
|
37514
37920
|
const page = (0, react.useSyncExternalStore)(pageStore.subscribe, pageStore.get);
|
|
37515
37921
|
const face = (0, react.useSyncExternalStore)(sessions.subscribe, () => page.sessionId ? sessions.face(page.sessionId) : void 0);
|
|
37516
37922
|
const chat = (0, react.useSyncExternalStore)(sessions.subscribe, () => page.sessionId ? sessions.chat(page.sessionId) : void 0);
|
|
@@ -37523,6 +37929,7 @@ function VideoPanel(props) {
|
|
|
37523
37929
|
page: true,
|
|
37524
37930
|
chatFace: face,
|
|
37525
37931
|
chatSource: chat,
|
|
37932
|
+
...videoHandlers(sessions, workspaces, page.sessionId, t("view.live")),
|
|
37526
37933
|
pending: page.pending,
|
|
37527
37934
|
onPendingDone: () => pageStore.set({ pending: void 0 })
|
|
37528
37935
|
}, page.sessionId)
|
|
@@ -37530,7 +37937,7 @@ function VideoPanel(props) {
|
|
|
37530
37937
|
}
|
|
37531
37938
|
/** Full-window video page (shell.overlay, older DS-Harness): player, subtitles, terms and the chat with DeepSeek. */
|
|
37532
37939
|
function VideoPage(props) {
|
|
37533
|
-
const { sessions, t } = props;
|
|
37940
|
+
const { sessions, workspaces, t } = props;
|
|
37534
37941
|
const page = (0, react.useSyncExternalStore)(pageStore.subscribe, pageStore.get);
|
|
37535
37942
|
const face = (0, react.useSyncExternalStore)(sessions.subscribe, () => page.sessionId ? sessions.face(page.sessionId) : void 0);
|
|
37536
37943
|
const chat = (0, react.useSyncExternalStore)(sessions.subscribe, () => page.sessionId ? sessions.chat(page.sessionId) : void 0);
|
|
@@ -37547,6 +37954,7 @@ function VideoPage(props) {
|
|
|
37547
37954
|
t,
|
|
37548
37955
|
chatFace: face,
|
|
37549
37956
|
chatSource: chat,
|
|
37957
|
+
...videoHandlers(sessions, workspaces, page.sessionId, t("view.live")),
|
|
37550
37958
|
onClose: () => closePage(sessions),
|
|
37551
37959
|
pending: page.pending,
|
|
37552
37960
|
onPendingDone: () => pageStore.set({ pending: void 0 })
|
|
@@ -37679,7 +38087,19 @@ function sessionsOf(ctx, extra) {
|
|
|
37679
38087
|
return;
|
|
37680
38088
|
}
|
|
37681
38089
|
},
|
|
37682
|
-
subscribe: (fn) => s.list.subscribe(fn)
|
|
38090
|
+
subscribe: (fn) => s.list.subscribe(fn),
|
|
38091
|
+
rename: (id, title) => {
|
|
38092
|
+
try {
|
|
38093
|
+
s.binding(id)?.session?.rename?.(title);
|
|
38094
|
+
} catch {}
|
|
38095
|
+
},
|
|
38096
|
+
info: (id) => {
|
|
38097
|
+
const x = list().byId?.[id];
|
|
38098
|
+
return x ? {
|
|
38099
|
+
title: x.title,
|
|
38100
|
+
cwd: x.cwd
|
|
38101
|
+
} : void 0;
|
|
38102
|
+
}
|
|
37683
38103
|
};
|
|
37684
38104
|
}
|
|
37685
38105
|
function apply(ctx) {
|
|
@@ -37738,6 +38158,7 @@ function apply(ctx) {
|
|
|
37738
38158
|
label: () => t("page.open"),
|
|
37739
38159
|
inject: () => ({
|
|
37740
38160
|
sessions: sessions(),
|
|
38161
|
+
workspaces: ctx.workspaces,
|
|
37741
38162
|
t
|
|
37742
38163
|
})
|
|
37743
38164
|
}, VideoPage));
|
|
@@ -37755,6 +38176,7 @@ function apply(ctx) {
|
|
|
37755
38176
|
locale: NS,
|
|
37756
38177
|
inject: () => ({
|
|
37757
38178
|
sessions: sessions(),
|
|
38179
|
+
workspaces: ctx.workspaces,
|
|
37758
38180
|
t
|
|
37759
38181
|
})
|
|
37760
38182
|
}, VideoPanel));
|
package/lib/index.mjs
CHANGED
|
@@ -2356,12 +2356,14 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2356
2356
|
const p = this.provider;
|
|
2357
2357
|
this.provider = void 0;
|
|
2358
2358
|
await p?.stop().catch(() => {});
|
|
2359
|
-
await this.translating;
|
|
2360
|
-
await this.polisher.flush();
|
|
2361
2359
|
this.subs = {
|
|
2362
2360
|
state,
|
|
2363
2361
|
at: this.subs.at
|
|
2364
2362
|
};
|
|
2363
|
+
this.persist();
|
|
2364
|
+
this.emit("state", this.summary());
|
|
2365
|
+
await this.translating;
|
|
2366
|
+
await this.polisher.flush();
|
|
2365
2367
|
this.save();
|
|
2366
2368
|
this.persist();
|
|
2367
2369
|
this.emit("state", this.summary());
|
|
@@ -2395,7 +2397,7 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2395
2397
|
this.emit("cue", cue);
|
|
2396
2398
|
if (cue.final) {
|
|
2397
2399
|
this.deps.glossary.notice(cue.src);
|
|
2398
|
-
if (this.opts.mode
|
|
2400
|
+
if (this.opts.mode !== "polished" && !this.provider?.translates && cue.tr === void 0 && this.targets().length) this.translate(cue);
|
|
2399
2401
|
else {
|
|
2400
2402
|
this.record(cue);
|
|
2401
2403
|
this.polisher.add(cue);
|
|
@@ -3419,13 +3421,21 @@ const ISO = {
|
|
|
3419
3421
|
var WhisperServer = class {
|
|
3420
3422
|
model;
|
|
3421
3423
|
log;
|
|
3424
|
+
audioCtx;
|
|
3422
3425
|
proc;
|
|
3423
3426
|
port;
|
|
3424
3427
|
starting;
|
|
3425
3428
|
users = 0;
|
|
3426
|
-
|
|
3429
|
+
/**
|
|
3430
|
+
* @param audioCtx Whisper's encoder window in frames (1500 = 30 s). Chunks
|
|
3431
|
+
* here are a few seconds long, and the encoder costs the same for 5 s as for
|
|
3432
|
+
* 30 s unless the window is cut down: a window just over the longest chunk
|
|
3433
|
+
* made recognition about 2.5× faster (Apple M5, large-v3-turbo).
|
|
3434
|
+
*/
|
|
3435
|
+
constructor(model, log, audioCtx = 0) {
|
|
3427
3436
|
this.model = model;
|
|
3428
3437
|
this.log = log;
|
|
3438
|
+
this.audioCtx = audioCtx;
|
|
3429
3439
|
}
|
|
3430
3440
|
async acquire() {
|
|
3431
3441
|
this.users++;
|
|
@@ -3448,7 +3458,8 @@ var WhisperServer = class {
|
|
|
3448
3458
|
String(port),
|
|
3449
3459
|
"-t",
|
|
3450
3460
|
"8",
|
|
3451
|
-
"-nt"
|
|
3461
|
+
"-nt",
|
|
3462
|
+
...this.audioCtx ? ["-ac", String(this.audioCtx)] : []
|
|
3452
3463
|
], { stdio: [
|
|
3453
3464
|
"ignore",
|
|
3454
3465
|
"ignore",
|
|
@@ -4615,7 +4626,7 @@ function apply(ctx, config) {
|
|
|
4615
4626
|
appendFileSync(logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}\n`);
|
|
4616
4627
|
} catch {}
|
|
4617
4628
|
};
|
|
4618
|
-
const whisper = new WhisperServer(config.whisperModel, log);
|
|
4629
|
+
const whisper = new WhisperServer(config.whisperModel, log, Math.min(1500, Math.max(256, Math.ceil((config.maxChunkSec + 5) * 50 / 64) * 64)));
|
|
4619
4630
|
const jobs = /* @__PURE__ */ new Map();
|
|
4620
4631
|
const byConversation = /* @__PURE__ */ new Map();
|
|
4621
4632
|
let llm;
|
|
@@ -4772,7 +4783,7 @@ function apply(ctx, config) {
|
|
|
4772
4783
|
tgtLangs: [config.defaultTargetLang],
|
|
4773
4784
|
srcLang: "auto",
|
|
4774
4785
|
provider: defaultEngine(),
|
|
4775
|
-
mode: "
|
|
4786
|
+
mode: "polished"
|
|
4776
4787
|
};
|
|
4777
4788
|
/** Browser notifications: "show this video" for a session (tools, /video). */
|
|
4778
4789
|
const bus = new EventEmitter();
|
|
@@ -4910,13 +4921,14 @@ function apply(ctx, config) {
|
|
|
4910
4921
|
/** The conversation's video: in memory, or the latest one on disk (after a restart). */
|
|
4911
4922
|
function jobForConversation(conversationId) {
|
|
4912
4923
|
const id = byConversation.get(conversationId);
|
|
4924
|
+
if (id === "") return void 0;
|
|
4913
4925
|
if (id && jobs.has(id)) return jobs.get(id);
|
|
4914
4926
|
const root = join(config.dataDir, "jobs");
|
|
4915
4927
|
if (!existsSync(root)) return void 0;
|
|
4916
4928
|
const dirs = readdirSync(root).filter((n) => /^[\w-]+$/.test(n)).sort().reverse();
|
|
4917
4929
|
for (const name of dirs) {
|
|
4918
4930
|
const file = join(root, name, "job.json");
|
|
4919
|
-
if (!existsSync(file)) continue;
|
|
4931
|
+
if (!existsSync(file) || existsSync(join(root, name, "closed"))) continue;
|
|
4920
4932
|
try {
|
|
4921
4933
|
if (JSON.parse(readFileSync(file, "utf8")).opts?.conversationId !== conversationId) continue;
|
|
4922
4934
|
} catch {
|
|
@@ -4931,6 +4943,7 @@ function apply(ctx, config) {
|
|
|
4931
4943
|
return job;
|
|
4932
4944
|
}
|
|
4933
4945
|
}
|
|
4946
|
+
const modeOf = (m) => m === "polished" || m === "site" ? m : "realtime";
|
|
4934
4947
|
async function startJob(opts, waiting = false) {
|
|
4935
4948
|
const prev = waiting ? void 0 : byConversation.get(opts.conversationId);
|
|
4936
4949
|
if (prev) await jobs.get(prev)?.close("replaced");
|
|
@@ -4941,7 +4954,7 @@ function apply(ctx, config) {
|
|
|
4941
4954
|
job = new LiveJob(id, opts, jobDeps(dir, opts, () => job, waiting));
|
|
4942
4955
|
jobs.set(id, job);
|
|
4943
4956
|
if (!waiting) byConversation.set(opts.conversationId, id);
|
|
4944
|
-
job.autoSubs =
|
|
4957
|
+
job.autoSubs = opts.mode !== "site";
|
|
4945
4958
|
bus.emit("job", job);
|
|
4946
4959
|
job.start();
|
|
4947
4960
|
return job;
|
|
@@ -5005,7 +5018,7 @@ function apply(ctx, config) {
|
|
|
5005
5018
|
tgtLangs: b.tgtLangs.map(String).slice(0, 4),
|
|
5006
5019
|
srcLang: typeof b.srcLang === "string" ? b.srcLang : "auto",
|
|
5007
5020
|
provider: typeof b.provider === "string" ? b.provider : "local",
|
|
5008
|
-
mode: b.mode
|
|
5021
|
+
mode: modeOf(b.mode)
|
|
5009
5022
|
} });
|
|
5010
5023
|
const source = b.source === "push" || b.source === "file" ? b.source : "url";
|
|
5011
5024
|
await disarm();
|
|
@@ -5013,13 +5026,25 @@ function apply(ctx, config) {
|
|
|
5013
5026
|
return send(res, 200, (await startJob({
|
|
5014
5027
|
source,
|
|
5015
5028
|
url: source === "file" && raw.startsWith("~") ? join(homedir(), raw.slice(1)) : raw,
|
|
5016
|
-
mode: b.mode
|
|
5029
|
+
mode: modeOf(b.mode),
|
|
5017
5030
|
srcLang: typeof b.srcLang === "string" ? b.srcLang : "auto",
|
|
5018
5031
|
tgtLangs: Array.isArray(b.tgtLangs) ? b.tgtLangs.filter((x) => typeof x === "string").slice(0, 4) : [config.defaultTargetLang],
|
|
5019
5032
|
provider: typeof b.provider === "string" ? b.provider : "local",
|
|
5020
5033
|
conversationId: String(b.conversationId ?? "default")
|
|
5021
5034
|
})).summary());
|
|
5022
5035
|
}
|
|
5036
|
+
if (route === "close" && req.method === "POST") {
|
|
5037
|
+
const b = await readJson(req);
|
|
5038
|
+
const job = jobs.get(String(b.jobId));
|
|
5039
|
+
if (!job) return send(res, 404, { error: "no such job" });
|
|
5040
|
+
await job.close("closed");
|
|
5041
|
+
const conv = job.summary().conversationId;
|
|
5042
|
+
if (byConversation.get(conv) === job.id) byConversation.set(conv, "");
|
|
5043
|
+
try {
|
|
5044
|
+
writeFileSync(join(job.summary().dir, "closed"), (/* @__PURE__ */ new Date()).toISOString());
|
|
5045
|
+
} catch {}
|
|
5046
|
+
return send(res, 200, { ok: true });
|
|
5047
|
+
}
|
|
5023
5048
|
if (route === "stop" && req.method === "POST") {
|
|
5024
5049
|
const b = await readJson(req);
|
|
5025
5050
|
const job = jobs.get(String(b.jobId));
|
|
@@ -5094,7 +5119,7 @@ function apply(ctx, config) {
|
|
|
5094
5119
|
if (Array.isArray(b.tgtLangs)) opts.tgtLangs = b.tgtLangs.map(String).slice(0, 4);
|
|
5095
5120
|
if (typeof b.srcLang === "string") opts.srcLang = b.srcLang;
|
|
5096
5121
|
if (typeof b.provider === "string") opts.provider = b.provider;
|
|
5097
|
-
if (b.mode === "realtime" || b.mode === "polished") opts.mode = b.mode;
|
|
5122
|
+
if (b.mode === "realtime" || b.mode === "polished" || b.mode === "site") opts.mode = b.mode;
|
|
5098
5123
|
const svc = opts.provider ? service(opts.provider) : void 0;
|
|
5099
5124
|
if (svc) {
|
|
5100
5125
|
await keys.refresh();
|
|
@@ -5113,7 +5138,7 @@ function apply(ctx, config) {
|
|
|
5113
5138
|
return send(res, 200, await arm({
|
|
5114
5139
|
source: "push",
|
|
5115
5140
|
url: "rtmp",
|
|
5116
|
-
mode: b.mode
|
|
5141
|
+
mode: modeOf(b.mode),
|
|
5117
5142
|
srcLang: typeof b.srcLang === "string" ? b.srcLang : "auto",
|
|
5118
5143
|
tgtLangs: Array.isArray(b.tgtLangs) ? b.tgtLangs.map(String).slice(0, 4) : [config.defaultTargetLang],
|
|
5119
5144
|
provider: typeof b.provider === "string" ? b.provider : "local",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-vidi",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.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",
|