dsh-vidi 0.4.2 → 0.5.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.
- package/lib/client.js +527 -144
- package/lib/index.mjs +74 -30
- package/package.json +1 -1
package/lib/index.mjs
CHANGED
|
@@ -1693,6 +1693,8 @@ const AHEAD_MAX_LINES = 10;
|
|
|
1693
1693
|
*
|
|
1694
1694
|
* Events: 'state' (JobSummary), 'cue' (Cue — new or revised).
|
|
1695
1695
|
*/
|
|
1696
|
+
/** Drop the first `skip` seconds after decoding (an output -ss; empty when there is nothing to drop). */
|
|
1697
|
+
const seekAfter = (skip) => skip > .01 ? ["-ss", skip.toFixed(3)] : [];
|
|
1696
1698
|
var LiveJob = class LiveJob extends EventEmitter {
|
|
1697
1699
|
opts;
|
|
1698
1700
|
deps;
|
|
@@ -2354,12 +2356,14 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2354
2356
|
const p = this.provider;
|
|
2355
2357
|
this.provider = void 0;
|
|
2356
2358
|
await p?.stop().catch(() => {});
|
|
2357
|
-
await this.translating;
|
|
2358
|
-
await this.polisher.flush();
|
|
2359
2359
|
this.subs = {
|
|
2360
2360
|
state,
|
|
2361
2361
|
at: this.subs.at
|
|
2362
2362
|
};
|
|
2363
|
+
this.persist();
|
|
2364
|
+
this.emit("state", this.summary());
|
|
2365
|
+
await this.translating;
|
|
2366
|
+
await this.polisher.flush();
|
|
2363
2367
|
this.save();
|
|
2364
2368
|
this.persist();
|
|
2365
2369
|
this.emit("state", this.summary());
|
|
@@ -2393,7 +2397,7 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2393
2397
|
this.emit("cue", cue);
|
|
2394
2398
|
if (cue.final) {
|
|
2395
2399
|
this.deps.glossary.notice(cue.src);
|
|
2396
|
-
if (this.opts.mode
|
|
2400
|
+
if (this.opts.mode !== "polished" && !this.provider?.translates && cue.tr === void 0 && this.targets().length) this.translate(cue);
|
|
2397
2401
|
else {
|
|
2398
2402
|
this.record(cue);
|
|
2399
2403
|
this.polisher.add(cue);
|
|
@@ -2594,7 +2598,14 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2594
2598
|
exportText(langs, format) {
|
|
2595
2599
|
return exportCues(this.cues, langs, format, this.title);
|
|
2596
2600
|
}
|
|
2597
|
-
/**
|
|
2601
|
+
/**
|
|
2602
|
+
* Best available picture source for time range [from, to]: original
|
|
2603
|
+
* download, finished recording, or recorded segments. `args` are the input
|
|
2604
|
+
* arguments; `skip` is how much to drop after decoding (output -ss). Files
|
|
2605
|
+
* seek exactly on input; live segments do not (a seek lands between
|
|
2606
|
+
* keyframes — YouTube live has one every 5 s — and decodes nothing), so they
|
|
2607
|
+
* are read from the start of the first segment and trimmed after decoding.
|
|
2608
|
+
*/
|
|
2598
2609
|
mediaFor(from, to) {
|
|
2599
2610
|
const original = readdirSync(this.deps.dir).find((n) => /^original\.(mp4|mkv|webm)$/.test(n));
|
|
2600
2611
|
if (original) return {
|
|
@@ -2604,7 +2615,7 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2604
2615
|
"-i",
|
|
2605
2616
|
join(this.deps.dir, original)
|
|
2606
2617
|
],
|
|
2607
|
-
|
|
2618
|
+
skip: 0
|
|
2608
2619
|
};
|
|
2609
2620
|
if (this.captureDone && existsSync(join(this.deps.dir, "recording.mp4"))) return {
|
|
2610
2621
|
args: [
|
|
@@ -2613,7 +2624,7 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2613
2624
|
"-i",
|
|
2614
2625
|
join(this.deps.dir, "recording.mp4")
|
|
2615
2626
|
],
|
|
2616
|
-
|
|
2627
|
+
skip: 0
|
|
2617
2628
|
};
|
|
2618
2629
|
const segs = this.segments();
|
|
2619
2630
|
const files = readFileSync(this.m3u8, "utf8").split("\n").filter((l) => l && !l.startsWith("#"));
|
|
@@ -2621,29 +2632,26 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2621
2632
|
let last = segs.findIndex((sg) => sg.start + sg.dur >= to);
|
|
2622
2633
|
if (last < 0) last = segs.length - 1;
|
|
2623
2634
|
if (!segs.length || first > last) throw new Error("that part has not been recorded yet");
|
|
2624
|
-
const list = files.slice(first, last + 1).map((f) => join(this.deps.dir, this.hlsName, f)).join("|");
|
|
2625
2635
|
return {
|
|
2626
|
-
args: [
|
|
2627
|
-
|
|
2628
|
-
String(from - segs[first].start),
|
|
2629
|
-
"-i",
|
|
2630
|
-
`concat:${list}`
|
|
2631
|
-
],
|
|
2632
|
-
offset: 0
|
|
2636
|
+
args: ["-i", `concat:${files.slice(first, last + 1).map((f) => join(this.deps.dir, this.hlsName, f)).join("|")}`],
|
|
2637
|
+
skip: Math.max(0, from - segs[first].start)
|
|
2633
2638
|
};
|
|
2634
2639
|
}
|
|
2635
2640
|
/** One small JPEG of the picture at media time `at`, for the model to look at (undefined for audio-only or on failure). */
|
|
2636
2641
|
async still(at) {
|
|
2637
2642
|
try {
|
|
2638
|
-
const { args } = this.mediaFor(at, at + 1);
|
|
2643
|
+
const { args, skip } = this.mediaFor(at, at + 1);
|
|
2639
2644
|
const { stdout } = await run$1("ffmpeg", [
|
|
2640
2645
|
"-v",
|
|
2641
2646
|
"error",
|
|
2642
2647
|
...args,
|
|
2648
|
+
...seekAfter(skip),
|
|
2643
2649
|
"-frames:v",
|
|
2644
2650
|
"1",
|
|
2645
2651
|
"-vf",
|
|
2646
2652
|
"scale=768:-2",
|
|
2653
|
+
"-pix_fmt",
|
|
2654
|
+
"yuvj420p",
|
|
2647
2655
|
"-q:v",
|
|
2648
2656
|
"5",
|
|
2649
2657
|
"-f",
|
|
@@ -2666,18 +2674,22 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2666
2674
|
const dir = join(this.deps.dir, "frames");
|
|
2667
2675
|
mkdirSync(dir, { recursive: true });
|
|
2668
2676
|
const out = join(dir, `frame_${at.toFixed(1).replace(".", "_")}.jpg`);
|
|
2669
|
-
const { args } = this.mediaFor(at, at + 1);
|
|
2677
|
+
const { args, skip } = this.mediaFor(at, at + 1);
|
|
2670
2678
|
await run$1("ffmpeg", [
|
|
2671
2679
|
"-v",
|
|
2672
2680
|
"error",
|
|
2673
2681
|
"-y",
|
|
2674
2682
|
...args,
|
|
2683
|
+
...seekAfter(skip),
|
|
2675
2684
|
"-frames:v",
|
|
2676
2685
|
"1",
|
|
2686
|
+
"-pix_fmt",
|
|
2687
|
+
"yuvj420p",
|
|
2677
2688
|
"-q:v",
|
|
2678
2689
|
"2",
|
|
2679
2690
|
out
|
|
2680
2691
|
], { timeout: 6e4 });
|
|
2692
|
+
if (!existsSync(out)) throw new Error("no picture at that moment");
|
|
2681
2693
|
return out;
|
|
2682
2694
|
}
|
|
2683
2695
|
/**
|
|
@@ -2690,14 +2702,14 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2690
2702
|
mkdirSync(dir, { recursive: true });
|
|
2691
2703
|
const name = `clip_${from.toFixed(1)}-${to.toFixed(1)}`.replace(/\./g, "_");
|
|
2692
2704
|
const out = join(dir, `${name}.mp4`);
|
|
2693
|
-
const { args } = this.mediaFor(from, to);
|
|
2705
|
+
const { args, skip } = this.mediaFor(from, to);
|
|
2694
2706
|
const subArgs = [];
|
|
2695
2707
|
const maps = [];
|
|
2696
2708
|
if (langs?.length) {
|
|
2697
2709
|
const inRange = this.cues.filter((c) => c.final && c.end > from && c.start < to).map((c) => ({
|
|
2698
2710
|
...c,
|
|
2699
|
-
start: Math.max(0, c.start - from),
|
|
2700
|
-
end: Math.min(to, c.end) - from
|
|
2711
|
+
start: Math.max(0, c.start - from) + skip,
|
|
2712
|
+
end: Math.min(to, c.end) - from + skip
|
|
2701
2713
|
}));
|
|
2702
2714
|
(langs.length > 1 ? [langs, ...langs.map((l) => [l])] : [langs]).forEach((ls, i) => {
|
|
2703
2715
|
const f = join(dir, `.${name}.${i}.srt`);
|
|
@@ -2712,6 +2724,7 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2712
2724
|
"-y",
|
|
2713
2725
|
...args,
|
|
2714
2726
|
...subArgs,
|
|
2727
|
+
...seekAfter(skip),
|
|
2715
2728
|
"-t",
|
|
2716
2729
|
String(to - from),
|
|
2717
2730
|
"-map",
|
|
@@ -2764,8 +2777,16 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2764
2777
|
rows
|
|
2765
2778
|
});
|
|
2766
2779
|
}
|
|
2780
|
+
const { args, skip } = this.mediaFor(from, to);
|
|
2767
2781
|
const pictures = /* @__PURE__ */ new Map();
|
|
2768
2782
|
const lines = ["ffconcat version 1.0"];
|
|
2783
|
+
if (skip > 0) {
|
|
2784
|
+
const blank = await drawSubtitles([], style, w, h);
|
|
2785
|
+
if (!blank) throw new Error("subtitles could not be drawn on this computer");
|
|
2786
|
+
const file = join(tmp, "blank.png");
|
|
2787
|
+
writeFileSync(file, blank);
|
|
2788
|
+
lines.push(`file '${file.replace(/'/g, "'\\''")}'`, `duration ${skip.toFixed(3)}`);
|
|
2789
|
+
}
|
|
2769
2790
|
for (const s of spans) {
|
|
2770
2791
|
let file = pictures.get(s.key);
|
|
2771
2792
|
if (!file) {
|
|
@@ -2780,7 +2801,6 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2780
2801
|
lines.push(`file '${pictures.get(spans.at(-1).key).replace(/'/g, "'\\''")}'`);
|
|
2781
2802
|
const list = join(tmp, "subs.ffconcat");
|
|
2782
2803
|
writeFileSync(list, lines.join("\n") + "\n");
|
|
2783
|
-
const { args } = this.mediaFor(from, to);
|
|
2784
2804
|
try {
|
|
2785
2805
|
await run$1("ffmpeg", [
|
|
2786
2806
|
"-v",
|
|
@@ -2793,6 +2813,7 @@ var LiveJob = class LiveJob extends EventEmitter {
|
|
|
2793
2813
|
"0",
|
|
2794
2814
|
"-i",
|
|
2795
2815
|
list,
|
|
2816
|
+
...seekAfter(skip),
|
|
2796
2817
|
"-t",
|
|
2797
2818
|
String(to - from),
|
|
2798
2819
|
"-filter_complex",
|
|
@@ -3400,13 +3421,21 @@ const ISO = {
|
|
|
3400
3421
|
var WhisperServer = class {
|
|
3401
3422
|
model;
|
|
3402
3423
|
log;
|
|
3424
|
+
audioCtx;
|
|
3403
3425
|
proc;
|
|
3404
3426
|
port;
|
|
3405
3427
|
starting;
|
|
3406
3428
|
users = 0;
|
|
3407
|
-
|
|
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) {
|
|
3408
3436
|
this.model = model;
|
|
3409
3437
|
this.log = log;
|
|
3438
|
+
this.audioCtx = audioCtx;
|
|
3410
3439
|
}
|
|
3411
3440
|
async acquire() {
|
|
3412
3441
|
this.users++;
|
|
@@ -3429,7 +3458,8 @@ var WhisperServer = class {
|
|
|
3429
3458
|
String(port),
|
|
3430
3459
|
"-t",
|
|
3431
3460
|
"8",
|
|
3432
|
-
"-nt"
|
|
3461
|
+
"-nt",
|
|
3462
|
+
...this.audioCtx ? ["-ac", String(this.audioCtx)] : []
|
|
3433
3463
|
], { stdio: [
|
|
3434
3464
|
"ignore",
|
|
3435
3465
|
"ignore",
|
|
@@ -4596,7 +4626,7 @@ function apply(ctx, config) {
|
|
|
4596
4626
|
appendFileSync(logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}\n`);
|
|
4597
4627
|
} catch {}
|
|
4598
4628
|
};
|
|
4599
|
-
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)));
|
|
4600
4630
|
const jobs = /* @__PURE__ */ new Map();
|
|
4601
4631
|
const byConversation = /* @__PURE__ */ new Map();
|
|
4602
4632
|
let llm;
|
|
@@ -4753,7 +4783,7 @@ function apply(ctx, config) {
|
|
|
4753
4783
|
tgtLangs: [config.defaultTargetLang],
|
|
4754
4784
|
srcLang: "auto",
|
|
4755
4785
|
provider: defaultEngine(),
|
|
4756
|
-
mode: "
|
|
4786
|
+
mode: "polished"
|
|
4757
4787
|
};
|
|
4758
4788
|
/** Browser notifications: "show this video" for a session (tools, /video). */
|
|
4759
4789
|
const bus = new EventEmitter();
|
|
@@ -4891,13 +4921,14 @@ function apply(ctx, config) {
|
|
|
4891
4921
|
/** The conversation's video: in memory, or the latest one on disk (after a restart). */
|
|
4892
4922
|
function jobForConversation(conversationId) {
|
|
4893
4923
|
const id = byConversation.get(conversationId);
|
|
4924
|
+
if (id === "") return void 0;
|
|
4894
4925
|
if (id && jobs.has(id)) return jobs.get(id);
|
|
4895
4926
|
const root = join(config.dataDir, "jobs");
|
|
4896
4927
|
if (!existsSync(root)) return void 0;
|
|
4897
4928
|
const dirs = readdirSync(root).filter((n) => /^[\w-]+$/.test(n)).sort().reverse();
|
|
4898
4929
|
for (const name of dirs) {
|
|
4899
4930
|
const file = join(root, name, "job.json");
|
|
4900
|
-
if (!existsSync(file)) continue;
|
|
4931
|
+
if (!existsSync(file) || existsSync(join(root, name, "closed"))) continue;
|
|
4901
4932
|
try {
|
|
4902
4933
|
if (JSON.parse(readFileSync(file, "utf8")).opts?.conversationId !== conversationId) continue;
|
|
4903
4934
|
} catch {
|
|
@@ -4912,6 +4943,7 @@ function apply(ctx, config) {
|
|
|
4912
4943
|
return job;
|
|
4913
4944
|
}
|
|
4914
4945
|
}
|
|
4946
|
+
const modeOf = (m) => m === "polished" || m === "site" ? m : "realtime";
|
|
4915
4947
|
async function startJob(opts, waiting = false) {
|
|
4916
4948
|
const prev = waiting ? void 0 : byConversation.get(opts.conversationId);
|
|
4917
4949
|
if (prev) await jobs.get(prev)?.close("replaced");
|
|
@@ -4922,7 +4954,7 @@ function apply(ctx, config) {
|
|
|
4922
4954
|
job = new LiveJob(id, opts, jobDeps(dir, opts, () => job, waiting));
|
|
4923
4955
|
jobs.set(id, job);
|
|
4924
4956
|
if (!waiting) byConversation.set(opts.conversationId, id);
|
|
4925
|
-
job.autoSubs =
|
|
4957
|
+
job.autoSubs = opts.mode !== "site";
|
|
4926
4958
|
bus.emit("job", job);
|
|
4927
4959
|
job.start();
|
|
4928
4960
|
return job;
|
|
@@ -4986,7 +5018,7 @@ function apply(ctx, config) {
|
|
|
4986
5018
|
tgtLangs: b.tgtLangs.map(String).slice(0, 4),
|
|
4987
5019
|
srcLang: typeof b.srcLang === "string" ? b.srcLang : "auto",
|
|
4988
5020
|
provider: typeof b.provider === "string" ? b.provider : "local",
|
|
4989
|
-
mode: b.mode
|
|
5021
|
+
mode: modeOf(b.mode)
|
|
4990
5022
|
} });
|
|
4991
5023
|
const source = b.source === "push" || b.source === "file" ? b.source : "url";
|
|
4992
5024
|
await disarm();
|
|
@@ -4994,13 +5026,25 @@ function apply(ctx, config) {
|
|
|
4994
5026
|
return send(res, 200, (await startJob({
|
|
4995
5027
|
source,
|
|
4996
5028
|
url: source === "file" && raw.startsWith("~") ? join(homedir(), raw.slice(1)) : raw,
|
|
4997
|
-
mode: b.mode
|
|
5029
|
+
mode: modeOf(b.mode),
|
|
4998
5030
|
srcLang: typeof b.srcLang === "string" ? b.srcLang : "auto",
|
|
4999
5031
|
tgtLangs: Array.isArray(b.tgtLangs) ? b.tgtLangs.filter((x) => typeof x === "string").slice(0, 4) : [config.defaultTargetLang],
|
|
5000
5032
|
provider: typeof b.provider === "string" ? b.provider : "local",
|
|
5001
5033
|
conversationId: String(b.conversationId ?? "default")
|
|
5002
5034
|
})).summary());
|
|
5003
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
|
+
}
|
|
5004
5048
|
if (route === "stop" && req.method === "POST") {
|
|
5005
5049
|
const b = await readJson(req);
|
|
5006
5050
|
const job = jobs.get(String(b.jobId));
|
|
@@ -5075,7 +5119,7 @@ function apply(ctx, config) {
|
|
|
5075
5119
|
if (Array.isArray(b.tgtLangs)) opts.tgtLangs = b.tgtLangs.map(String).slice(0, 4);
|
|
5076
5120
|
if (typeof b.srcLang === "string") opts.srcLang = b.srcLang;
|
|
5077
5121
|
if (typeof b.provider === "string") opts.provider = b.provider;
|
|
5078
|
-
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;
|
|
5079
5123
|
const svc = opts.provider ? service(opts.provider) : void 0;
|
|
5080
5124
|
if (svc) {
|
|
5081
5125
|
await keys.refresh();
|
|
@@ -5094,7 +5138,7 @@ function apply(ctx, config) {
|
|
|
5094
5138
|
return send(res, 200, await arm({
|
|
5095
5139
|
source: "push",
|
|
5096
5140
|
url: "rtmp",
|
|
5097
|
-
mode: b.mode
|
|
5141
|
+
mode: modeOf(b.mode),
|
|
5098
5142
|
srcLang: typeof b.srcLang === "string" ? b.srcLang : "auto",
|
|
5099
5143
|
tgtLangs: Array.isArray(b.tgtLangs) ? b.tgtLangs.map(String).slice(0, 4) : [config.defaultTargetLang],
|
|
5100
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.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",
|