dsh-vidi 0.5.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/lib/client.js +162 -41
  2. package/lib/index.mjs +537 -20
  3. package/package.json +1 -1
package/lib/client.js CHANGED
@@ -821,12 +821,14 @@ function Pick(props) {
821
821
  const room = window.innerHeight - r.bottom - 8, above = r.top - 8;
822
822
  const up = room < 220 && above > room;
823
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);
824
+ const minWidth = Math.min(Math.max(r.width, 180), window.innerWidth - 16);
825
+ const maxWidth = Math.max(minWidth, Math.min(380, window.innerWidth - 16));
826
+ const side = r.left + maxWidth > window.innerWidth - 8 ? { right: Math.max(8, window.innerWidth - r.right) } : { left: Math.max(8, r.left) };
826
827
  setPlace({
827
828
  ...vars,
828
- left,
829
- width,
829
+ ...side,
830
+ minWidth,
831
+ maxWidth,
830
832
  maxHeight: maxH,
831
833
  ...up ? { bottom: window.innerHeight - r.top + 4 } : { top: r.bottom + 4 },
832
834
  fontFamily: cs.fontFamily
@@ -34605,6 +34607,19 @@ function RestreamPanel(props) {
34605
34607
  }
34606
34608
 
34607
34609
  // src/client/Settings.tsx
34610
+ /** Who runs a speech engine (engines of one vendor share its key). */
34611
+ const vendorOf = (p) => p.vendor ?? p.id;
34612
+ /**
34613
+ * Subtitle models worth suggesting first: the fast, cheap tier of each family
34614
+ * (flash / mini / turbo / air / lite / haiku), newest first, and the one DSH
34615
+ * uses by default.
34616
+ */
34617
+ function recommended(models, fallback) {
34618
+ const ver = (n) => Number(/(\d+(?:\.\d+)?)/.exec(n)?.[1] ?? 0);
34619
+ const picks = models.filter((m) => /flash|mini|turbo|\bair\b|-air|lite|haiku/i.test(`${m.model} ${m.name}`) && !/embed|tts|audio|image|vision-preview|realtime/i.test(m.model)).sort((a, b) => ver(b.model) - ver(a.model)).slice(0, 2).map((m) => m.model);
34620
+ if (fallback && models.some((m) => m.model === fallback)) picks.unshift(fallback);
34621
+ return new Set(picks);
34622
+ }
34608
34623
  const engineLabel = (t, id) => {
34609
34624
  const v = t(`engine.${id}`);
34610
34625
  return v === `engine.${id}` ? id : v;
@@ -34628,6 +34643,13 @@ function Settings(props) {
34628
34643
  const [msg, setMsg] = (0, react.useState)(null);
34629
34644
  const cur = providers.find((p) => p.id === engine);
34630
34645
  const key = cur?.key ?? { set: false };
34646
+ const vendorName = (v) => {
34647
+ const x = t(`vendor.${v}`);
34648
+ return x === `vendor.${v}` ? name(v) : x;
34649
+ };
34650
+ const vendors = [...new Map(providers.map((p) => [vendorOf(p), p])).values()];
34651
+ const curVendor = cur ? vendorOf(cur) : "local";
34652
+ const vendorEngines = providers.filter((p) => vendorOf(p) === curVendor);
34631
34653
  (0, react.useEffect)(() => {
34632
34654
  if (!open) return;
34633
34655
  const close = (e) => {
@@ -34646,9 +34668,6 @@ function Settings(props) {
34646
34668
  fetch("/live/api/models").then((r) => r.json()).then(setModels).catch(() => {});
34647
34669
  }, [open, models]);
34648
34670
  const modelKey = (m) => m ? `${m.provider}/${m.model}` : "";
34649
- const modelName = (m) => {
34650
- return (models?.models.find((x) => modelKey(x) === modelKey(m)))?.name ?? m?.model ?? "";
34651
- };
34652
34671
  const chooseModel = (key) => {
34653
34672
  const row = models?.models.find((x) => modelKey(x) === key);
34654
34673
  const subsModel = row ? {
@@ -34661,8 +34680,14 @@ function Settings(props) {
34661
34680
  });
34662
34681
  post$1("settings", { subsModel });
34663
34682
  };
34664
- const providersOf = [...new Set((models?.models ?? []).map((m) => m.providerName))];
34665
- const current = models?.chosen ?? models?.fallback;
34683
+ const current = models?.chosen ?? models?.fallback ?? models?.current ?? void 0;
34684
+ const providersOf = [...new Map([...current && !(models?.models ?? []).some((m) => m.provider === current.provider) ? [[current.provider, {
34685
+ id: current.provider,
34686
+ name: current.provider
34687
+ }]] : [], ...(models?.models ?? []).map((m) => [m.provider, {
34688
+ id: m.provider,
34689
+ name: m.providerName
34690
+ }])]).values()];
34666
34691
  const sees = !!models?.models.find((x) => modelKey(x) === modelKey(current))?.image;
34667
34692
  const saveKey = async (value) => {
34668
34693
  if (!cur) return;
@@ -34702,19 +34727,35 @@ function Settings(props) {
34702
34727
  className: "dshl-pop settings right",
34703
34728
  children: [
34704
34729
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
34705
- className: "srow",
34730
+ className: "srow col",
34706
34731
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
34707
34732
  title: t("set.engine.tip"),
34708
34733
  children: t("set.engine")
34709
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pick, {
34710
- value: engine,
34711
- disabled,
34712
- onChange: onEngine,
34713
- options: providers.map((p) => ({
34714
- value: p.id,
34715
- label: name(p.id),
34716
- hint: p.ready ? void 0 : t(p.online ? "set.engine.noKey" : "set.engine.notInstalled")
34717
- }))
34734
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
34735
+ className: "duo",
34736
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pick, {
34737
+ value: curVendor,
34738
+ disabled,
34739
+ onChange: (v) => {
34740
+ const first = providers.find((p) => vendorOf(p) === v);
34741
+ if (first) onEngine(first.id);
34742
+ },
34743
+ options: vendors.map((p) => ({
34744
+ value: vendorOf(p),
34745
+ label: vendorName(vendorOf(p)),
34746
+ hint: p.ready ? void 0 : t(p.online ? "set.engine.noKey" : "set.engine.notInstalled"),
34747
+ group: p.online ? t(p.region === "cn" ? "set.engine.cn" : "set.engine.intl") : void 0
34748
+ }))
34749
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pick, {
34750
+ value: engine,
34751
+ disabled: disabled || vendorEngines.length < 2,
34752
+ onChange: onEngine,
34753
+ options: vendorEngines.map((p, i) => ({
34754
+ value: p.id,
34755
+ label: p.model ?? name(p.id),
34756
+ hint: t(`engine.note.${p.id}`) !== `engine.note.${p.id}` ? t(`engine.note.${p.id}`) : i === 0 && vendorEngines.length > 1 ? t("set.recommended") : void 0
34757
+ }))
34758
+ })]
34718
34759
  })]
34719
34760
  }),
34720
34761
  cur?.online && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -34722,7 +34763,7 @@ function Settings(props) {
34722
34763
  children: [
34723
34764
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
34724
34765
  title: t(shared ? "set.key.tip.shared" : "set.key.tip"),
34725
- children: t("set.key").replace("{s}", name(cur.id))
34766
+ children: t("set.key").replace("{s}", vendorName(curVendor))
34726
34767
  }),
34727
34768
  key.set && !draft && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
34728
34769
  className: "keyline",
@@ -34783,28 +34824,60 @@ function Settings(props) {
34783
34824
  }),
34784
34825
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
34785
34826
  className: "srow col",
34786
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
34787
- className: "pair",
34788
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
34827
+ children: [
34828
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
34789
34829
  title: t("set.model.tip"),
34790
34830
  children: t("set.model")
34791
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pick, {
34792
- value: models?.chosen ? modelKey(models.chosen) : "",
34793
- onChange: chooseModel,
34794
- options: [{
34795
- value: "",
34796
- label: models?.fallback ? t("set.model.default").replace("{m}", modelName(models.fallback)) : t("set.model.dsh")
34797
- }, ...providersOf.flatMap((pn) => models.models.filter((m) => m.providerName === pn).map((m) => ({
34798
- value: modelKey(m),
34799
- label: m.name,
34800
- hint: m.image ? t("set.model.image") : void 0,
34801
- group: pn
34802
- })))]
34803
- })]
34804
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
34805
- className: "hint",
34806
- children: t(sees ? "set.model.sees" : "set.model.text")
34807
- })]
34831
+ }),
34832
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
34833
+ className: "duo",
34834
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pick, {
34835
+ value: current?.provider ?? "",
34836
+ placeholder: "…",
34837
+ onChange: (v) => {
34838
+ const list = (models?.models ?? []).filter((m) => m.provider === v);
34839
+ const rec = recommended(list, models?.fallback?.provider === v ? models.fallback.model : void 0);
34840
+ const pick = list.find((m) => rec.has(m.model)) ?? list[0];
34841
+ if (pick) chooseModel(modelKey(pick));
34842
+ },
34843
+ options: providersOf.map((p) => ({
34844
+ value: p.id,
34845
+ label: p.name
34846
+ }))
34847
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Pick, {
34848
+ value: modelKey(current),
34849
+ placeholder: "…",
34850
+ onChange: chooseModel,
34851
+ options: (() => {
34852
+ const listed = (models?.models ?? []).filter((m) => m.provider === current?.provider);
34853
+ const list = current && !listed.some((m) => m.model === current.model) ? [{
34854
+ provider: current.provider,
34855
+ providerName: current.provider,
34856
+ model: current.model,
34857
+ name: current.model,
34858
+ image: false
34859
+ }, ...listed] : listed;
34860
+ const rec = recommended(list, models?.fallback?.provider === current?.provider ? models?.fallback?.model : void 0);
34861
+ const tag = (m) => [modelKey(m) === modelKey(models?.fallback) ? t("set.model.dshDefault") : "", m.image ? t("set.model.image") : ""].filter(Boolean).join(" · ") || void 0;
34862
+ return [...list.filter((m) => rec.has(m.model)).map((m) => ({
34863
+ value: modelKey(m),
34864
+ label: m.name,
34865
+ hint: tag(m),
34866
+ group: t("set.recommended")
34867
+ })), ...list.filter((m) => !rec.has(m.model)).map((m) => ({
34868
+ value: modelKey(m),
34869
+ label: m.name,
34870
+ hint: tag(m),
34871
+ group: rec.size ? t("set.allModels") : void 0
34872
+ }))];
34873
+ })()
34874
+ })]
34875
+ }),
34876
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
34877
+ className: "hint",
34878
+ children: t(sees ? "set.model.sees" : "set.model.text")
34879
+ })
34880
+ ]
34808
34881
  }),
34809
34882
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
34810
34883
  className: "srow col",
@@ -36848,8 +36921,31 @@ const en = {
36848
36921
  "set.engine.notInstalled": "not installed",
36849
36922
  "engine.local": "This computer (Whisper)",
36850
36923
  "engine.openai": "OpenAI",
36924
+ "engine.openai-translate": "OpenAI live translation",
36925
+ "set.engine.intl": "Online",
36926
+ "set.recommended": "Recommended",
36927
+ "set.allModels": "All models",
36928
+ "set.model.dshDefault": "DSH default",
36929
+ "engine.note.openai-translate": "listens and translates",
36930
+ "vendor.local": "This computer",
36931
+ "vendor.openai": "OpenAI",
36932
+ "vendor.gemini": "Google",
36933
+ "vendor.deepgram": "Deepgram",
36934
+ "vendor.elevenlabs": "ElevenLabs",
36935
+ "vendor.mistral": "Mistral",
36936
+ "vendor.groq": "Groq",
36937
+ "vendor.dashscope": "Alibaba Cloud Bailian",
36938
+ "vendor.volc": "Volcengine",
36939
+ "vendor.siliconflow": "SiliconFlow",
36940
+ "vendor.glm": "Zhipu",
36941
+ "set.engine.cn": "Online · mainland China",
36851
36942
  "engine.gemini": "Google Gemini",
36943
+ "engine.deepgram": "Deepgram",
36944
+ "engine.elevenlabs": "ElevenLabs",
36945
+ "engine.mistral": "Mistral (Voxtral)",
36852
36946
  "engine.groq": "Groq",
36947
+ "engine.dashscope": "Alibaba Cloud (Qwen-ASR)",
36948
+ "engine.volc": "Volcengine (Doubao)",
36853
36949
  "engine.siliconflow": "SiliconFlow",
36854
36950
  "engine.glm": "Zhipu GLM",
36855
36951
  "set.local.missing": "Local recognition (whisper.cpp) isn’t installed on this computer. Pick an online service above and add its key.",
@@ -37184,8 +37280,31 @@ const zh = {
37184
37280
  "set.engine.notInstalled": "未安装",
37185
37281
  "engine.local": "本机(Whisper)",
37186
37282
  "engine.openai": "OpenAI",
37283
+ "engine.openai-translate": "OpenAI 实时翻译",
37284
+ "set.engine.intl": "在线 · 海外",
37285
+ "set.recommended": "推荐",
37286
+ "set.allModels": "全部模型",
37287
+ "set.model.dshDefault": "DSH 默认",
37288
+ "engine.note.openai-translate": "边听边翻",
37289
+ "vendor.local": "本机",
37290
+ "vendor.openai": "OpenAI",
37291
+ "vendor.gemini": "Google",
37292
+ "vendor.deepgram": "Deepgram",
37293
+ "vendor.elevenlabs": "ElevenLabs",
37294
+ "vendor.mistral": "Mistral",
37295
+ "vendor.groq": "Groq",
37296
+ "vendor.dashscope": "阿里云百炼",
37297
+ "vendor.volc": "火山引擎",
37298
+ "vendor.siliconflow": "硅基流动",
37299
+ "vendor.glm": "智谱",
37300
+ "set.engine.cn": "在线 · 国内",
37187
37301
  "engine.gemini": "Google Gemini",
37302
+ "engine.deepgram": "Deepgram",
37303
+ "engine.elevenlabs": "ElevenLabs",
37304
+ "engine.mistral": "Mistral(Voxtral)",
37188
37305
  "engine.groq": "Groq",
37306
+ "engine.dashscope": "阿里云百炼(Qwen-ASR)",
37307
+ "engine.volc": "火山引擎(豆包语音)",
37189
37308
  "engine.siliconflow": "硅基流动",
37190
37309
  "engine.glm": "智谱 GLM",
37191
37310
  "set.local.missing": "这台电脑没装本地识别(whisper.cpp)。在上面换一个在线服务,填上它的密钥就能用。",
@@ -37795,7 +37914,9 @@ html[data-platform='darwin'] .dshl.page .dshl-top :is(button, input, label, a, .
37795
37914
  .dshl-pop.right { left: auto; right: 0; }
37796
37915
  .dshl-pop.up { top: auto; bottom: calc(100% + 6px); }
37797
37916
  .dshl .srow { display: flex; align-items: center; justify-content: space-between; gap: 12px; min-height: 46px; font-size: 13.5px; }
37798
- .dshl-pop.settings { width: 320px; padding: 6px 12px; }
37917
+ .dshl-pop.settings { width: 380px; padding: 6px 12px; }
37918
+ .dshl .srow .duo { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 3fr); gap: 8px; }
37919
+ .dshl .srow .duo > .dshl-select { width: 100%; }
37799
37920
  .dshl-pop.settings .srow + .srow { border-top: 1px solid var(--line-soft); }
37800
37921
  .dshl-pop.settings .srow > .dshl-select, .dshl-pop.settings .srow > .dshl-seg { width: 168px; }
37801
37922
  .dshl-pop.settings .srow > .dshl-seg button { flex: 1; padding: 0 6px; }
package/lib/index.mjs CHANGED
@@ -2979,7 +2979,7 @@ var LiveJob = class LiveJob extends EventEmitter {
2979
2979
  // src/downloads.ts
2980
2980
  const run = promisify(execFile);
2981
2981
  /** ISO 639-2 codes for MP4 subtitle tracks. */
2982
- const ISO3 = {
2982
+ const ISO3$1 = {
2983
2983
  zh: "chi",
2984
2984
  "zh-Hant": "chi",
2985
2985
  en: "eng",
@@ -3125,7 +3125,7 @@ var Downloads = class {
3125
3125
  const file = join(tmp, `${i}.srt`);
3126
3126
  writeFileSync(file, exportCues(cues, t.langs, "srt", title));
3127
3127
  inputs.push("-i", file);
3128
- meta.push(`-metadata:s:s:${i}`, `language=${ISO3[t.code] ?? ISO3[t.code.slice(0, 2)] ?? "und"}`, `-metadata:s:s:${i}`, `title=${t.name}`, `-disposition:s:${i}`, i === 0 ? "default" : "0");
3128
+ meta.push(`-metadata:s:s:${i}`, `language=${ISO3$1[t.code] ?? ISO3$1[t.code.slice(0, 2)] ?? "und"}`, `-metadata:s:s:${i}`, `title=${t.name}`, `-disposition:s:${i}`, i === 0 ? "default" : "0");
3129
3129
  });
3130
3130
  try {
3131
3131
  await run("ffmpeg", [
@@ -3300,7 +3300,12 @@ const KEY_REFS = {
3300
3300
  "GLM_API_KEY",
3301
3301
  "ZHIPUAI_API_KEY",
3302
3302
  "ZAI_API_KEY"
3303
- ]
3303
+ ],
3304
+ deepgram: ["DEEPGRAM_API_KEY"],
3305
+ elevenlabs: ["ELEVENLABS_API_KEY", "XI_API_KEY"],
3306
+ mistral: ["MISTRAL_API_KEY"],
3307
+ dashscope: ["DASHSCOPE_API_KEY", "BAILIAN_API_KEY"],
3308
+ volc: ["VOLC_SPEECH_API_KEY", "VOLCENGINE_SPEECH_API_KEY"]
3304
3309
  };
3305
3310
  const KEY_NAMES = Object.keys(KEY_REFS);
3306
3311
  const mask = (k) => k.length > 10 ? `${k.slice(0, 3)}…${k.slice(-4)}` : "••••";
@@ -4071,39 +4076,107 @@ var OpenAIProvider = class {
4071
4076
  const SERVICES = [
4072
4077
  {
4073
4078
  id: "openai",
4079
+ key: "openai",
4074
4080
  label: "OpenAI",
4075
4081
  keyUrl: "https://platform.openai.com/api-keys",
4076
4082
  model: "gpt-live-transcribe",
4083
+ region: "intl",
4077
4084
  kind: "realtime"
4078
4085
  },
4086
+ {
4087
+ id: "openai-translate",
4088
+ key: "openai",
4089
+ label: "OpenAI live translation",
4090
+ keyUrl: "https://platform.openai.com/api-keys",
4091
+ model: "gpt-realtime-translate",
4092
+ region: "intl",
4093
+ kind: "translate"
4094
+ },
4079
4095
  {
4080
4096
  id: "gemini",
4097
+ key: "gemini",
4081
4098
  label: "Google Gemini",
4082
4099
  keyUrl: "https://aistudio.google.com/apikey",
4083
4100
  model: "auto",
4101
+ modelLabel: "gemini-3.5-transcribe",
4102
+ region: "intl",
4084
4103
  kind: "gemini"
4085
4104
  },
4105
+ {
4106
+ id: "deepgram",
4107
+ key: "deepgram",
4108
+ label: "Deepgram",
4109
+ keyUrl: "https://console.deepgram.com/",
4110
+ model: "nova-3",
4111
+ region: "intl",
4112
+ kind: "deepgram"
4113
+ },
4114
+ {
4115
+ id: "elevenlabs",
4116
+ key: "elevenlabs",
4117
+ label: "ElevenLabs",
4118
+ keyUrl: "https://elevenlabs.io/app/settings/api-keys",
4119
+ model: "scribe_v2",
4120
+ region: "intl",
4121
+ kind: "elevenlabs"
4122
+ },
4123
+ {
4124
+ id: "mistral",
4125
+ key: "mistral",
4126
+ label: "Mistral",
4127
+ keyUrl: "https://console.mistral.ai/api-keys",
4128
+ model: "voxtral-mini-latest",
4129
+ region: "intl",
4130
+ kind: "whisper-api",
4131
+ base: "https://api.mistral.ai/v1"
4132
+ },
4086
4133
  {
4087
4134
  id: "groq",
4135
+ key: "groq",
4088
4136
  label: "Groq",
4089
4137
  keyUrl: "https://console.groq.com/keys",
4090
4138
  model: "whisper-large-v3-turbo",
4139
+ region: "intl",
4091
4140
  kind: "whisper-api",
4092
4141
  base: "https://api.groq.com/openai/v1"
4093
4142
  },
4143
+ {
4144
+ id: "dashscope",
4145
+ key: "dashscope",
4146
+ label: "Alibaba Cloud Bailian",
4147
+ keyUrl: "https://bailian.console.aliyun.com/?tab=model#/api-key",
4148
+ model: "qwen3-asr-flash",
4149
+ region: "cn",
4150
+ kind: "dashscope",
4151
+ base: "https://dashscope.aliyuncs.com/compatible-mode/v1"
4152
+ },
4153
+ {
4154
+ id: "volc",
4155
+ key: "volc",
4156
+ label: "Volcengine Doubao",
4157
+ keyUrl: "https://console.volcengine.com/speech/new/",
4158
+ model: "volc.bigasr.auc_turbo",
4159
+ modelLabel: "Seed-ASR 极速版",
4160
+ region: "cn",
4161
+ kind: "volc"
4162
+ },
4094
4163
  {
4095
4164
  id: "siliconflow",
4165
+ key: "siliconflow",
4096
4166
  label: "SiliconFlow",
4097
4167
  keyUrl: "https://cloud.siliconflow.cn/account/ak",
4098
4168
  model: "FunAudioLLM/SenseVoiceSmall",
4169
+ region: "cn",
4099
4170
  kind: "whisper-api",
4100
4171
  base: "https://api.siliconflow.cn/v1"
4101
4172
  },
4102
4173
  {
4103
4174
  id: "glm",
4175
+ key: "glm",
4104
4176
  label: "Zhipu GLM",
4105
4177
  keyUrl: "https://bigmodel.cn/usercenter/proj-mgmt/apikeys",
4106
4178
  model: "glm-asr-2512",
4179
+ region: "cn",
4107
4180
  kind: "whisper-api",
4108
4181
  base: "https://open.bigmodel.cn/api/paas/v4"
4109
4182
  }
@@ -4123,6 +4196,8 @@ function speechServiceOf(id, baseURL) {
4123
4196
  if (/api\.groq\.com|^ groq$/.test(k)) return "groq";
4124
4197
  if (/siliconflow/.test(k)) return "siliconflow";
4125
4198
  if (/bigmodel\.cn|(^|\.)z\.ai|^ (glm|zhipu|zhipuai|zai)$/.test(k)) return "glm";
4199
+ if (/api\.mistral\.ai|^ mistral$/.test(k)) return "mistral";
4200
+ if (/dashscope|maas\.aliyuncs\.com|^ (dashscope|bailian|aliyun|qwen)$/.test(k)) return "dashscope";
4126
4201
  }
4127
4202
  const TIMEOUT = 3e4;
4128
4203
  async function fail(r) {
@@ -4176,6 +4251,172 @@ function whisperApiEngine(svc, model, key) {
4176
4251
  }
4177
4252
  };
4178
4253
  }
4254
+ /** Glossary terms from a recogniser hint ("Glossary: a, b, c. …"), for services that take a word list. */
4255
+ const termsOf = (prompt) => (/Glossary: ([^\n]*?)\.(?:\s|$)/.exec(prompt)?.[1] ?? "").split(", ").map((x) => x.trim()).filter(Boolean);
4256
+ /** Three-letter language codes some services answer with, to the two-letter ones used here. */
4257
+ const ISO3 = {
4258
+ eng: "en",
4259
+ zho: "zh",
4260
+ cmn: "zh",
4261
+ yue: "yue",
4262
+ jpn: "ja",
4263
+ kor: "ko",
4264
+ fra: "fr",
4265
+ deu: "de",
4266
+ spa: "es",
4267
+ por: "pt",
4268
+ rus: "ru",
4269
+ ita: "it",
4270
+ ara: "ar",
4271
+ hin: "hi",
4272
+ vie: "vi",
4273
+ tha: "th",
4274
+ ind: "id"
4275
+ };
4276
+ const lang2 = (l) => {
4277
+ if (!l) return void 0;
4278
+ const k = l.toLowerCase().split(/[-_]/)[0];
4279
+ return ISO3[k] ?? ISO[k] ?? k;
4280
+ };
4281
+ /** Deepgram (Nova-3): one request per chunk, glossary as key terms, language detected per chunk. */
4282
+ function deepgramEngine(model, key) {
4283
+ return {
4284
+ name: "deepgram",
4285
+ async transcribe(wav, lang, prompt) {
4286
+ const apiKey = key();
4287
+ if (!apiKey) throw new Error("no Deepgram key");
4288
+ const q = new URLSearchParams({
4289
+ model,
4290
+ smart_format: "true",
4291
+ punctuate: "true"
4292
+ });
4293
+ if (lang !== "auto") q.set("language", lang);
4294
+ else q.set("detect_language", "true");
4295
+ for (const t of termsOf(prompt ?? "").slice(0, 50)) q.append("keyterm", t);
4296
+ const r = await fetch(`https://api.deepgram.com/v1/listen?${q}`, {
4297
+ method: "POST",
4298
+ headers: {
4299
+ Authorization: `Token ${apiKey}`,
4300
+ "content-type": "audio/wav"
4301
+ },
4302
+ body: new Uint8Array(wav),
4303
+ signal: AbortSignal.timeout(TIMEOUT)
4304
+ });
4305
+ if (!r.ok) await fail(r);
4306
+ const ch = (await r.json()).results?.channels?.[0];
4307
+ return {
4308
+ text: (ch?.alternatives?.[0]?.transcript ?? "").trim(),
4309
+ lang: lang2(ch?.detected_language)
4310
+ };
4311
+ }
4312
+ };
4313
+ }
4314
+ /** ElevenLabs Scribe: multipart upload per chunk; glossary as key terms. */
4315
+ function elevenLabsEngine(model, key) {
4316
+ return {
4317
+ name: "elevenlabs",
4318
+ async transcribe(wav, lang, prompt) {
4319
+ const apiKey = key();
4320
+ if (!apiKey) throw new Error("no ElevenLabs key");
4321
+ const form = new FormData();
4322
+ form.append("file", new Blob([new Uint8Array(wav)], { type: "audio/wav" }), "chunk.wav");
4323
+ form.append("model_id", model);
4324
+ form.append("tag_audio_events", "false");
4325
+ if (lang !== "auto") form.append("language_code", lang);
4326
+ for (const t of termsOf(prompt ?? "").slice(0, 50)) form.append("keyterms", t);
4327
+ const r = await fetch("https://api.elevenlabs.io/v1/speech-to-text", {
4328
+ method: "POST",
4329
+ headers: { "xi-api-key": apiKey },
4330
+ body: form,
4331
+ signal: AbortSignal.timeout(TIMEOUT)
4332
+ });
4333
+ if (!r.ok) await fail(r);
4334
+ const j = await r.json();
4335
+ return {
4336
+ text: (j.text ?? "").replace(/\([^)]*\)/g, "").replace(/\s+/g, " ").trim(),
4337
+ lang: lang2(j.language_code)
4338
+ };
4339
+ }
4340
+ };
4341
+ }
4342
+ /** Alibaba Cloud Bailian (Qwen3-ASR-Flash), OpenAI-compatible chat call with the chunk inline. */
4343
+ function dashscopeEngine(base, model, key) {
4344
+ return {
4345
+ name: "dashscope",
4346
+ async transcribe(wav, lang) {
4347
+ const apiKey = key();
4348
+ if (!apiKey) throw new Error("no Alibaba Cloud Bailian key");
4349
+ const body = {
4350
+ model,
4351
+ stream: false,
4352
+ messages: [{
4353
+ role: "user",
4354
+ content: [{
4355
+ type: "input_audio",
4356
+ input_audio: { data: `data:audio/wav;base64,${wav.toString("base64")}` }
4357
+ }]
4358
+ }],
4359
+ asr_options: {
4360
+ enable_itn: true,
4361
+ ...lang !== "auto" ? { language: lang } : {}
4362
+ }
4363
+ };
4364
+ const r = await fetch(`${base}/chat/completions`, {
4365
+ method: "POST",
4366
+ headers: {
4367
+ Authorization: `Bearer ${apiKey}`,
4368
+ "content-type": "application/json"
4369
+ },
4370
+ body: JSON.stringify(body),
4371
+ signal: AbortSignal.timeout(TIMEOUT)
4372
+ });
4373
+ if (!r.ok) await fail(r);
4374
+ const m = (await r.json()).choices?.[0]?.message;
4375
+ return {
4376
+ text: (m?.content ?? "").replace(/\s+/g, " ").trim(),
4377
+ lang: lang2(m?.annotations?.[0]?.language)
4378
+ };
4379
+ }
4380
+ };
4381
+ }
4382
+ /** Volcengine Doubao speech (recording recognition, flash edition): one request per chunk, API key from the new speech console. */
4383
+ function volcEngine(resource, key) {
4384
+ return {
4385
+ name: "volc",
4386
+ async transcribe(wav) {
4387
+ const apiKey = key();
4388
+ if (!apiKey) throw new Error("no Volcengine speech key");
4389
+ const body = {
4390
+ user: { uid: "dsh-vidi" },
4391
+ audio: { data: wav.toString("base64") },
4392
+ request: {
4393
+ model_name: "bigmodel",
4394
+ enable_itn: true,
4395
+ enable_punc: true
4396
+ }
4397
+ };
4398
+ const r = await fetch("https://openspeech.bytedance.com/api/v3/auc/bigmodel/recognize/flash", {
4399
+ method: "POST",
4400
+ headers: {
4401
+ "X-Api-Key": apiKey,
4402
+ "X-Api-Resource-Id": resource,
4403
+ "X-Api-Request-Id": crypto.randomUUID(),
4404
+ "X-Api-Sequence": "-1",
4405
+ "content-type": "application/json"
4406
+ },
4407
+ body: JSON.stringify(body),
4408
+ signal: AbortSignal.timeout(TIMEOUT)
4409
+ });
4410
+ const code = r.headers.get("X-Api-Status-Code");
4411
+ if (code === "20000003") return { text: "" };
4412
+ if (!r.ok || code && code !== "20000000") {
4413
+ const msg = r.headers.get("X-Api-Message") ?? (await r.text().catch(() => "")).slice(0, 200);
4414
+ throw new Error(`Volcengine ${code ?? r.status}: ${msg}`);
4415
+ }
4416
+ return { text: ((await r.json()).result?.text ?? "").trim() };
4417
+ }
4418
+ };
4419
+ }
4179
4420
  /** Newest Gemini Flash model the key can use (when the setting says "auto"). */
4180
4421
  async function pickGeminiModel(apiKey) {
4181
4422
  const r = await fetch("https://generativelanguage.googleapis.com/v1beta/models?pageSize=200", {
@@ -4183,7 +4424,10 @@ async function pickGeminiModel(apiKey) {
4183
4424
  signal: AbortSignal.timeout(TIMEOUT)
4184
4425
  });
4185
4426
  if (!r.ok) await fail(r);
4186
- const flash = ((await r.json()).models ?? []).filter((m) => m.supportedGenerationMethods?.includes("generateContent")).map((m) => m.name.replace(/^models\//, "")).filter((n) => /^gemini-\d+(\.\d+)?-flash(-preview)?$/.test(n));
4427
+ const j = await r.json();
4428
+ const transcribe = (j.models ?? []).map((m) => m.name.replace(/^models\//, "")).filter((n) => /^gemini-\d+(\.\d+)?-transcribe$/.test(n)).sort((a, b) => Number(/\d+(\.\d+)?/.exec(b)[0]) - Number(/\d+(\.\d+)?/.exec(a)[0]));
4429
+ if (transcribe[0]) return transcribe[0];
4430
+ const flash = (j.models ?? []).filter((m) => m.supportedGenerationMethods?.includes("generateContent")).map((m) => m.name.replace(/^models\//, "")).filter((n) => /^gemini-\d+(\.\d+)?-flash(-preview)?$/.test(n));
4187
4431
  const version = (n) => Number(/^gemini-(\d+(?:\.\d+)?)/.exec(n)[1]);
4188
4432
  flash.sort((a, b) => version(b) - version(a) || (a.includes("preview") ? 1 : 0) - (b.includes("preview") ? 1 : 0));
4189
4433
  return flash[0] ?? "gemini-2.5-flash";
@@ -4210,6 +4454,7 @@ function geminiEngine(model, key, onUsage, log) {
4210
4454
  async transcribe(wav, lang, prompt) {
4211
4455
  const apiKey = key();
4212
4456
  if (!apiKey) throw new Error("no Google Gemini key");
4457
+ if (/-transcribe$/.test(chosen)) return transcribeModel(apiKey, chosen, wav, lang, prompt ?? "", onUsage);
4213
4458
  const instruction = [
4214
4459
  "Transcribe the speech in this audio exactly as spoken, in the language spoken (do not translate).",
4215
4460
  lang !== "auto" ? `The speaker uses language code "${lang}".` : "",
@@ -4271,20 +4516,95 @@ function geminiEngine(model, key, onUsage, log) {
4271
4516
  }
4272
4517
  };
4273
4518
  }
4519
+ const BCP47 = {
4520
+ zh: "zh-CN",
4521
+ en: "en-US",
4522
+ ja: "ja-JP",
4523
+ ko: "ko-KR",
4524
+ fr: "fr-FR",
4525
+ de: "de-DE",
4526
+ es: "es-ES",
4527
+ pt: "pt-BR",
4528
+ ru: "ru-RU",
4529
+ it: "it-IT"
4530
+ };
4531
+ /** Gemini's dedicated transcription model (Interactions API): verbatim text, glossary as custom vocabulary. */
4532
+ async function transcribeModel(apiKey, model, wav, lang, prompt, onUsage) {
4533
+ const vocab = termsOf(prompt).slice(0, 100);
4534
+ const body = {
4535
+ model: `models/${model}`,
4536
+ input: [{
4537
+ type: "audio",
4538
+ data: wav.toString("base64"),
4539
+ mime_type: "audio/wav"
4540
+ }],
4541
+ generation_config: { transcription_config: {
4542
+ ...lang !== "auto" ? { language_codes: [BCP47[lang] ?? lang] } : {},
4543
+ ...vocab.length ? { custom_vocabulary: vocab } : {},
4544
+ mode: { type: "verbatim" }
4545
+ } }
4546
+ };
4547
+ const r = await fetch("https://generativelanguage.googleapis.com/v1beta/interactions", {
4548
+ method: "POST",
4549
+ headers: {
4550
+ "x-goog-api-key": apiKey,
4551
+ "content-type": "application/json"
4552
+ },
4553
+ body: JSON.stringify(body),
4554
+ signal: AbortSignal.timeout(TIMEOUT)
4555
+ });
4556
+ if (!r.ok) await fail(r);
4557
+ const j = await r.json();
4558
+ if (j.usage) onUsage(model, {
4559
+ inputTokens: j.usage.total_input_tokens,
4560
+ outputTokens: j.usage.total_output_tokens
4561
+ });
4562
+ const clean = (j.steps ?? []).filter((st) => st.type === "model_output").flatMap((st) => st.content ?? []).filter((c) => c.type === "text").map((c) => c.text ?? "").join("").replace(/\s+/g, " ").trim();
4563
+ return {
4564
+ text: clean,
4565
+ lang: lang !== "auto" ? lang : scriptLang(clean)
4566
+ };
4567
+ }
4568
+ /** The language a transcript is written in, when its script says so (kana, hangul, Han); else unknown. */
4569
+ function scriptLang(text) {
4570
+ const n = (re) => (text.match(re) ?? []).length;
4571
+ const han = n(/\p{Script=Han}/gu), kana = n(/[\p{Script=Hiragana}\p{Script=Katakana}]/gu), hangul = n(/\p{Script=Hangul}/gu);
4572
+ const letters = n(/\p{L}/gu);
4573
+ if (!letters) return void 0;
4574
+ if (kana > letters * .1) return "ja";
4575
+ if (hangul > letters * .3) return "ko";
4576
+ if (han > letters * .3) return "zh";
4577
+ }
4274
4578
  /** Check a key before saving it: a cheap authenticated read on the service. */
4275
4579
  async function verifyKey(svc, key) {
4276
4580
  try {
4277
- const url = svc.kind === "gemini" ? "https://generativelanguage.googleapis.com/v1beta/models?pageSize=1" : svc.id === "openai" ? "https://api.openai.com/v1/models" : svc.id === "glm" ? null : `${svc.base}/models`;
4278
- if (!url) return { ok: true };
4279
- const r = await fetch(url, {
4280
- headers: svc.kind === "gemini" ? { "x-goog-api-key": key } : { Authorization: `Bearer ${key}` },
4581
+ const probe = svc.kind === "gemini" ? {
4582
+ url: "https://generativelanguage.googleapis.com/v1beta/models?pageSize=1",
4583
+ headers: { "x-goog-api-key": key }
4584
+ } : svc.key === "openai" ? {
4585
+ url: "https://api.openai.com/v1/models",
4586
+ headers: { Authorization: `Bearer ${key}` }
4587
+ } : svc.kind === "deepgram" ? {
4588
+ url: "https://api.deepgram.com/v1/projects",
4589
+ headers: { Authorization: `Token ${key}` }
4590
+ } : svc.kind === "elevenlabs" ? {
4591
+ url: "https://api.elevenlabs.io/v1/models",
4592
+ headers: { "xi-api-key": key }
4593
+ } : svc.id === "glm" || svc.kind === "volc" ? null : {
4594
+ url: `${svc.base}/models`,
4595
+ headers: { Authorization: `Bearer ${key}` }
4596
+ };
4597
+ if (!probe) return { ok: true };
4598
+ const r = await fetch(probe.url, {
4599
+ headers: probe.headers,
4281
4600
  signal: AbortSignal.timeout(1e4)
4282
4601
  });
4283
4602
  if (r.ok) return { ok: true };
4284
4603
  const body = await r.json().catch(() => ({}));
4604
+ const detail = typeof body.detail === "object" ? body.detail?.message : body.detail;
4285
4605
  return {
4286
4606
  ok: false,
4287
- message: (typeof body.error === "object" ? body.error?.message : body.error) ?? body.message ?? `HTTP ${r.status}`
4607
+ message: (typeof body.error === "object" ? body.error?.message : body.error) ?? body.message ?? detail ?? body.err_msg ?? `HTTP ${r.status}`
4288
4608
  };
4289
4609
  } catch (err) {
4290
4610
  return {
@@ -4293,6 +4613,185 @@ async function verifyKey(svc, key) {
4293
4613
  };
4294
4614
  }
4295
4615
  }
4616
+ // src/providers/openai-translate.ts
4617
+ /** Output languages gpt-realtime-translate speaks (the input can be any of 70+). */
4618
+ const TRANSLATE_TARGETS = [
4619
+ "es",
4620
+ "pt",
4621
+ "fr",
4622
+ "ja",
4623
+ "ru",
4624
+ "zh",
4625
+ "de",
4626
+ "ko",
4627
+ "hi",
4628
+ "id",
4629
+ "vi",
4630
+ "it",
4631
+ "en"
4632
+ ];
4633
+ /**
4634
+ * OpenAI live interpretation (gpt-realtime-translate): audio streams in, the
4635
+ * translation and the source transcript stream back as text, so no separate
4636
+ * translation pass is needed. The service has no turns of its own, so the
4637
+ * shared pause detector decides where a subtitle ends; text that arrives
4638
+ * shortly after a pause still belongs to the line before it.
4639
+ */
4640
+ var OpenAITranslateProvider = class {
4641
+ opts;
4642
+ name = "openai-translate";
4643
+ translates = true;
4644
+ ws;
4645
+ chunker;
4646
+ up = new Upsampler();
4647
+ emit;
4648
+ onError;
4649
+ stopped = false;
4650
+ reconnects = 0;
4651
+ /** Text of the line being collected (source transcript and translation). */
4652
+ src = "";
4653
+ tr = "";
4654
+ lineStart = 0;
4655
+ seq = 0;
4656
+ /** A finished stretch of audio whose text may still be arriving. */
4657
+ closing;
4658
+ constructor(opts) {
4659
+ this.opts = opts;
4660
+ if (!TRANSLATE_TARGETS.includes(opts.target)) throw new Error(`OpenAI live translation speaks only ${TRANSLATE_TARGETS.join(", ")}; choose one of these as the subtitle language, or another engine`);
4661
+ this.chunker = new PauseChunker(opts.minSec, opts.maxSec, (c) => this.onChunk(c));
4662
+ }
4663
+ async start(emit, onError) {
4664
+ this.emit = emit;
4665
+ this.onError = onError;
4666
+ await this.connect();
4667
+ }
4668
+ connect() {
4669
+ return new Promise((resolve, reject) => {
4670
+ const url = this.opts.url ?? `wss://api.openai.com/v1/realtime/translations?model=${encodeURIComponent(this.opts.model)}`;
4671
+ const ws = new WebSocket(url, { headers: { Authorization: `Bearer ${this.opts.apiKey}` } });
4672
+ this.ws = ws;
4673
+ let opened = false;
4674
+ ws.onopen = () => {
4675
+ opened = true;
4676
+ this.reconnects = 0;
4677
+ this.send({
4678
+ type: "session.update",
4679
+ session: { audio: {
4680
+ input: {
4681
+ transcription: { model: "gpt-realtime-whisper" },
4682
+ noise_reduction: { type: "far_field" }
4683
+ },
4684
+ output: { language: this.opts.target }
4685
+ } }
4686
+ });
4687
+ resolve();
4688
+ };
4689
+ ws.onmessage = (ev) => {
4690
+ try {
4691
+ this.onEvent(JSON.parse(String(ev.data)));
4692
+ } catch {}
4693
+ };
4694
+ ws.onerror = () => {
4695
+ if (!opened) reject(/* @__PURE__ */ new Error("could not reach OpenAI live translation (check the API key and network)"));
4696
+ };
4697
+ ws.onclose = (ev) => {
4698
+ if (!opened || this.stopped || this.ws !== ws) return;
4699
+ this.opts.log?.(`openai-translate: connection closed (${ev.code} ${ev.reason || ""}), reconnecting`);
4700
+ if (++this.reconnects > 5) {
4701
+ this.onError?.(/* @__PURE__ */ new Error("OpenAI live translation keeps dropping"));
4702
+ return;
4703
+ }
4704
+ setTimeout(() => {
4705
+ if (!this.stopped) this.connect().catch((err) => this.onError?.(err));
4706
+ }, 1e3 * this.reconnects);
4707
+ };
4708
+ });
4709
+ }
4710
+ send(event) {
4711
+ if (this.ws?.readyState === WebSocket.OPEN) this.ws.send(JSON.stringify(event));
4712
+ }
4713
+ push(pcm) {
4714
+ if (this.stopped) return;
4715
+ this.send({
4716
+ type: "session.input_audio_buffer.append",
4717
+ audio: this.up.push(Buffer.from(pcm)).toString("base64")
4718
+ });
4719
+ this.chunker.push(pcm);
4720
+ }
4721
+ backlog() {
4722
+ return this.closing ? this.chunker.now - this.closing.start : 0;
4723
+ }
4724
+ draft() {
4725
+ if (!this.src.trim() && !this.tr.trim()) return;
4726
+ this.emit?.({
4727
+ key: `ot:${this.seq}`,
4728
+ start: this.lineStart,
4729
+ end: Math.max(this.chunker.now, this.lineStart + .5),
4730
+ src: this.src.trim(),
4731
+ tr: this.tr.trim() ? { [this.opts.target]: this.tr.trim() } : void 0,
4732
+ final: false
4733
+ });
4734
+ }
4735
+ /** A pause ended a stretch: wait a moment for its last words, then finish the line. */
4736
+ onChunk({ start, end }) {
4737
+ if (this.closing) this.finish();
4738
+ if (!this.src.trim() && !this.tr.trim()) {
4739
+ this.lineStart = end;
4740
+ return;
4741
+ }
4742
+ this.lineStart = start;
4743
+ this.closing = {
4744
+ start,
4745
+ end,
4746
+ timer: setTimeout(() => this.finish(), 1500)
4747
+ };
4748
+ }
4749
+ finish() {
4750
+ const c = this.closing;
4751
+ if (!c) return;
4752
+ clearTimeout(c.timer);
4753
+ this.closing = void 0;
4754
+ const src = this.src.trim(), tr = this.tr.trim();
4755
+ if (src || tr) this.emit?.({
4756
+ key: `ot:${this.seq}`,
4757
+ start: c.start,
4758
+ end: Math.max(c.end, c.start + .5),
4759
+ src: src || tr,
4760
+ tr: tr ? { [this.opts.target]: tr } : void 0,
4761
+ final: true
4762
+ });
4763
+ this.seq++;
4764
+ this.src = "";
4765
+ this.tr = "";
4766
+ this.lineStart = c.end;
4767
+ }
4768
+ onEvent(ev) {
4769
+ switch (ev.type) {
4770
+ case "session.input_transcript.delta":
4771
+ this.src += String(ev.delta ?? "");
4772
+ this.draft();
4773
+ break;
4774
+ case "session.output_transcript.delta":
4775
+ this.tr += String(ev.delta ?? "");
4776
+ this.draft();
4777
+ break;
4778
+ case "error": {
4779
+ const err = ev.error ?? {};
4780
+ this.onError?.(/* @__PURE__ */ new Error(`OpenAI: ${err.message ?? "error"}`));
4781
+ break;
4782
+ }
4783
+ }
4784
+ }
4785
+ async stop() {
4786
+ this.chunker.flush(.5);
4787
+ await new Promise((r) => setTimeout(r, 1500));
4788
+ this.finish();
4789
+ this.stopped = true;
4790
+ try {
4791
+ this.ws?.close();
4792
+ } catch {}
4793
+ }
4794
+ };
4296
4795
  // src/tools.ts
4297
4796
  const CLOCK = (s) => {
4298
4797
  const h = Math.floor(s / 3600), m = Math.floor(s / 60) % 60, ss = Math.floor(s % 60);
@@ -4831,7 +5330,7 @@ function apply(ctx, config) {
4831
5330
  ].includes(x.name)).every((x) => x.ok);
4832
5331
  });
4833
5332
  /** Recognition to use when nothing was chosen: this computer if it can, else the first online service with a key. */
4834
- const defaultEngine = () => localReady ? "local" : SERVICES.find((s) => keys.get(s.id))?.id ?? "local";
5333
+ const defaultEngine = () => localReady ? "local" : SERVICES.find((s) => keys.get(s.key))?.id ?? "local";
4835
5334
  const settingsFile = join(config.dataDir, "settings.json");
4836
5335
  const prefs = () => {
4837
5336
  try {
@@ -4865,7 +5364,7 @@ function apply(ctx, config) {
4865
5364
  function makeProvider(opts, glossary, usage, job) {
4866
5365
  const svc = service(opts.provider);
4867
5366
  if (svc) {
4868
- if (!keys.get(svc.id)) throw new Error(`no ${svc.label} key yet: add one in the video page settings or on the DSH Models page`);
5367
+ if (!keys.get(svc.key)) throw new Error(`no ${svc.label} key yet: add one in the video page settings or on the DSH Models page`);
4869
5368
  if (svc.kind === "realtime") return new OpenAIProvider({
4870
5369
  apiKey: keys.get("openai"),
4871
5370
  model: config.openaiModel,
@@ -4877,11 +5376,24 @@ function apply(ctx, config) {
4877
5376
  log,
4878
5377
  onUsage: (u) => usage.add("openai", config.openaiModel, u)
4879
5378
  });
4880
- const key = () => keys.get(svc.id);
5379
+ if (svc.kind === "translate") {
5380
+ const target = opts.tgtLangs[0];
5381
+ if (!target) throw new Error("OpenAI live translation needs a subtitle language to translate into");
5382
+ return new OpenAITranslateProvider({
5383
+ apiKey: keys.get("openai"),
5384
+ model: svc.model,
5385
+ target,
5386
+ minSec: 2,
5387
+ maxSec: 8,
5388
+ log
5389
+ });
5390
+ }
5391
+ const key = () => keys.get(svc.key);
5392
+ const base = keys.bases.get(svc.key) ?? svc.base;
4881
5393
  return new LocalProvider({
4882
- engine: svc.kind === "gemini" ? geminiEngine(config.geminiModel, key, (model, u) => usage.add("gemini", model, u), log) : whisperApiEngine({
5394
+ engine: svc.kind === "gemini" ? geminiEngine(config.geminiModel, key, (model, u) => usage.add("gemini", model, u), log) : svc.kind === "deepgram" ? deepgramEngine(svc.model, key) : svc.kind === "elevenlabs" ? elevenLabsEngine(svc.model, key) : svc.kind === "dashscope" ? dashscopeEngine(base, svc.model, key) : svc.kind === "volc" ? volcEngine(svc.model, key) : whisperApiEngine({
4883
5395
  ...svc,
4884
- base: keys.bases.get(svc.id) ?? svc.base
5396
+ base
4885
5397
  }, svc.model, key),
4886
5398
  lang: opts.srcLang === "auto" ? "auto" : opts.srcLang,
4887
5399
  minSec: 3,
@@ -5052,14 +5564,19 @@ function apply(ctx, config) {
5052
5564
  dataDir: config.dataDir,
5053
5565
  providers: [{
5054
5566
  id: "local",
5567
+ vendor: "local",
5568
+ model: "whisper-large-v3-turbo",
5055
5569
  online: false,
5056
5570
  ready: localReady
5057
5571
  }, ...SERVICES.map((s) => ({
5058
5572
  id: s.id,
5573
+ vendor: s.key,
5574
+ model: s.modelLabel ?? s.model,
5059
5575
  label: s.label,
5060
5576
  online: true,
5061
- ready: keys.info(s.id).set,
5062
- key: keys.info(s.id),
5577
+ region: s.region,
5578
+ ready: keys.info(s.key).set,
5579
+ key: keys.info(s.key),
5063
5580
  keyUrl: s.keyUrl
5064
5581
  }))],
5065
5582
  defaultEngine: defaultEngine(),
@@ -5076,10 +5593,10 @@ function apply(ctx, config) {
5076
5593
  const check = await verifyKey(svc, key);
5077
5594
  if (!check.ok) return send(res, 400, { error: check.message ?? "key rejected" });
5078
5595
  }
5079
- await keys.save(svc.id, key);
5080
- return send(res, 200, { key: keys.info(svc.id) });
5596
+ await keys.save(svc.key, key);
5597
+ return send(res, 200, { key: keys.info(svc.key) });
5081
5598
  }
5082
- const key = keys.get(svc.id);
5599
+ const key = keys.get(svc.key);
5083
5600
  if (!key) return send(res, 400, { error: "no key" });
5084
5601
  const check = await verifyKey(svc, key);
5085
5602
  return send(res, check.ok ? 200 : 400, check.ok ? { ok: true } : { error: check.message });
@@ -5196,7 +5713,7 @@ function apply(ctx, config) {
5196
5713
  const svc = opts.provider ? service(opts.provider) : void 0;
5197
5714
  if (svc) {
5198
5715
  await keys.refresh();
5199
- if (!keys.get(svc.id)) return send(res, 400, { error: `no ${svc.label} key` });
5716
+ if (!keys.get(svc.key)) return send(res, 400, { error: `no ${svc.label} key` });
5200
5717
  }
5201
5718
  savePrefs({ last: {
5202
5719
  ...lastOptions(),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-vidi",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "A video tool for DeepSeek Harness: watch live streams, videos and podcasts with real-time subtitles in any language, then clip and download",
5
5
  "type": "module",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",