@lalalic/markcut 3.2.4 → 3.2.6
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/.env.example +10 -0
- package/README.md +28 -0
- package/docs/markdown-strict-descriptive.md +16 -0
- package/package.json +1 -1
- package/src/config.mjs +6 -3
- package/src/descriptive/markdown.test.ts +38 -0
- package/src/vision/cli.mjs +7 -2
- package/src/vision/default-infer.mjs +101 -0
- package/tests/vision-default-infer.test.ts +83 -0
package/.env.example
CHANGED
|
@@ -25,3 +25,13 @@ GOOGLE_MAPS_API_KEY=your_key_here
|
|
|
25
25
|
# MARKCUT_MAX_VIDEO_DURATION=60
|
|
26
26
|
# MARKCUT_MAX_VIDEO_DIMENSION=360
|
|
27
27
|
# MARKCUT_VTT_SAMPLE_INTERVAL=5
|
|
28
|
+
# ── Default Vision backend policy ────────────────────────────────────────────
|
|
29
|
+
# Browser ChatGPT Temporary Chat is attempted first when this command exists.
|
|
30
|
+
# MARKCUT_CHATGPT_BROWSER_INFER_CLI=/path/to/chatgpt-browser-infer
|
|
31
|
+
# Codex fallback profiles are tried left-to-right; `default` means no -p flag.
|
|
32
|
+
# MARKCUT_CODEX_VISION_PROFILES=zai,default
|
|
33
|
+
# MARKCUT_CHATGPT_VISION_TIMEOUT_MS=600000
|
|
34
|
+
# MARKCUT_CODEX_VISION_TIMEOUT_MS=600000
|
|
35
|
+
# Local MLX is final fallback only.
|
|
36
|
+
# MARKCUT_LOCAL_VISION_MODEL=mlx-community/MiniCPM-V-4.6-bf16
|
|
37
|
+
# MARKCUT_LOCAL_VISION_TIMEOUT_MS=600000
|
package/README.md
CHANGED
|
@@ -341,4 +341,32 @@ flowchart LR
|
|
|
341
341
|
| `--dry-run` | Show what would be processed without running AI |
|
|
342
342
|
| `--show-prompts` | Print the prompts template file and exit |
|
|
343
343
|
|
|
344
|
+
### Default Vision backend priority
|
|
345
|
+
|
|
346
|
+
Markcut Vision is remote-first by default so ordinary perception does not require a heavy local VLM on the host Mac. Explicit `--itt` / `--vtt` flags and `MARKCUT_ITT_CLI` / `MARKCUT_VTT_CLI` environment variables still override these defaults.
|
|
347
|
+
|
|
348
|
+
Default ITT tries, in order:
|
|
349
|
+
|
|
350
|
+
1. **Browser ChatGPT Temporary Chat** through `chatgpt-browser-infer` (or `MARKCUT_CHATGPT_BROWSER_INFER_CLI`). The Neo runtime owns browser automation, isolation, attachment readiness, retries, and cleanup.
|
|
351
|
+
2. **Codex CLI remote vision**, trying profiles from `MARKCUT_CODEX_VISION_PROFILES` (default `zai,default`). On this Mac that means the lightweight Z.ai profile first, then the normal Codex/OpenAI profile.
|
|
352
|
+
3. **Local MLX MiniCPM** as the final fallback only. Override its model with `MARKCUT_LOCAL_VISION_MODEL`.
|
|
353
|
+
|
|
354
|
+
Default VTT is intentionally empty. Markcut therefore performs its existing deterministic chronological frame extraction (5–10 representative frames, controlled by `--vtt-sample-interval`) and sends those frames through the same ITT priority chain. Set `--vtt` or `MARKCUT_VTT_CLI` when a dedicated video backend is explicitly desired.
|
|
355
|
+
|
|
356
|
+
To expose the Neo Browser ChatGPT runtime on this Mac:
|
|
357
|
+
|
|
358
|
+
```bash
|
|
359
|
+
ln -sf "$HOME/Workspace/neo/skills/chatgpt-browser-worker/bin/chatgpt-browser-infer" "$HOME/.local/bin/chatgpt-browser-infer"
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
Useful policy overrides:
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
export MARKCUT_CODEX_VISION_PROFILES='zai,default'
|
|
366
|
+
export MARKCUT_CHATGPT_BROWSER_INFER_CLI='/path/to/chatgpt-browser-infer'
|
|
367
|
+
# Explicit custom backends still win:
|
|
368
|
+
export MARKCUT_ITT_CLI='my-image-backend --prompt "{prompt}" {input}'
|
|
369
|
+
export MARKCUT_VTT_CLI='my-video-backend --prompt "{prompt}" {input}'
|
|
370
|
+
```
|
|
371
|
+
|
|
344
372
|
## Architecture
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
|
|
2
|
+
## HTML comments: ignored metadata
|
|
3
|
+
|
|
4
|
+
Standard Markdown HTML comments are intentionally ignored by Markcut's descriptive parser and never enter the descriptive tree or rendered output:
|
|
5
|
+
|
|
6
|
+
```md
|
|
7
|
+
<!-- arbitrary metadata -->
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
This makes comments suitable for **out-of-band metadata owned by another tool**, for example:
|
|
11
|
+
|
|
12
|
+
```md
|
|
13
|
+
<!-- execution {"id":"profiles-demo","type":"demo","scene_id":"profiles","output":"assets/profiles-demo.mp4"} -->
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Markcut does **not** parse, validate, or interpret the comment contents. It only guarantees that HTML comment nodes are ignored. A separate tool may read its own comment convention directly from the Markdown source.
|
package/package.json
CHANGED
package/src/config.mjs
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Default CLI templates and constants for the markcut pipeline.
|
|
3
5
|
*
|
|
@@ -96,7 +98,8 @@ export const DEFAULT_EDIT_CLI = args.cliOverrides.editCli || process.env.MARKCUT
|
|
|
96
98
|
export const DEFAULT_TTI_CLI = args.cliOverrides.tti || process.env.MARKCUT_TTI_CLI || 'uvx --from mflux mflux-generate-flux2 --model flux2-klein-4b --steps 2 --prompt "{input}" --output "{output}" --seed {seed}';
|
|
97
99
|
/** Text-to-video CLI. Override via --ttv flag or MARKCUT_TTV_CLI env var. */
|
|
98
100
|
export const DEFAULT_TTV_CLI = args.cliOverrides.ttv || process.env.MARKCUT_TTV_CLI || '';
|
|
101
|
+
const DEFAULT_VISION_INFER = fileURLToPath(new URL("./vision/default-infer.mjs", import.meta.url));
|
|
99
102
|
/** Image-to-text CLI. Override via --itt flag or MARKCUT_ITT_CLI env var. */
|
|
100
|
-
export const DEFAULT_ITT_CLI = args.cliOverrides.itt || process.env.MARKCUT_ITT_CLI ||
|
|
101
|
-
/** Video-to-text CLI. Override via --vtt flag or MARKCUT_VTT_CLI env var. */
|
|
102
|
-
export const DEFAULT_VTT_CLI = args.cliOverrides.vtt || process.env.MARKCUT_VTT_CLI || '
|
|
103
|
+
export const DEFAULT_ITT_CLI = args.cliOverrides.itt || process.env.MARKCUT_ITT_CLI || `node ${JSON.stringify(DEFAULT_VISION_INFER)} --prompt "{prompt}" {input}`;
|
|
104
|
+
/** Video-to-text CLI. Override via --vtt flag or MARKCUT_VTT_CLI env var. Empty by default so Vision uses chronological frame extraction through ITT. */
|
|
105
|
+
export const DEFAULT_VTT_CLI = args.cliOverrides.vtt || process.env.MARKCUT_VTT_CLI || '';
|
|
@@ -864,3 +864,41 @@ layout:parallel
|
|
|
864
864
|
});
|
|
865
865
|
});
|
|
866
866
|
});
|
|
867
|
+
|
|
868
|
+
// HTML comments are an intentional out-of-band metadata channel. remark-parse
|
|
869
|
+
// emits them as `html` nodes; Markcut ignores those nodes completely.
|
|
870
|
+
describe("HTML comments", () => {
|
|
871
|
+
it("ignores generic HTML comments", () => {
|
|
872
|
+
const doc = `# video
|
|
873
|
+
<!-- arbitrary metadata -->
|
|
874
|
+
## Scene
|
|
875
|
+
- image src:a.jpg duration:1`;
|
|
876
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
877
|
+
expect(parsed.children).toHaveLength(1);
|
|
878
|
+
const scene = parsed.children[0] as any;
|
|
879
|
+
expect(scene.name).toBe("Scene");
|
|
880
|
+
expect(scene.children).toHaveLength(1);
|
|
881
|
+
expect(scene.children[0].type).toBe("image");
|
|
882
|
+
});
|
|
883
|
+
|
|
884
|
+
it("ignores execution metadata comments between scene metadata and media", () => {
|
|
885
|
+
const doc = `# video
|
|
886
|
+
width:1920 height:1080 fps:30 layout:series
|
|
887
|
+
|
|
888
|
+
## profiles
|
|
889
|
+
title:"Reveal profiles" instruction:"Each child has a distinct profile"
|
|
890
|
+
<!-- execution {"id":"profiles-demo","type":"demo","scene_id":"profiles","output":"assets/profiles-demo.mp4"} -->
|
|
891
|
+
- video src:"assets/profiles-demo.mp4" duration:6`;
|
|
892
|
+
const parsed = parseMarkdownDescriptive(doc);
|
|
893
|
+
const scene = parsed.children[0] as any;
|
|
894
|
+
expect(scene.name).toBe("profiles");
|
|
895
|
+
expect(scene.title).toBe("Reveal profiles");
|
|
896
|
+
expect(scene.instruction).toBe("Each child has a distinct profile");
|
|
897
|
+
expect(scene.children).toHaveLength(1);
|
|
898
|
+
expect(scene.children[0]).toMatchObject({
|
|
899
|
+
type: "video",
|
|
900
|
+
src: "assets/profiles-demo.mp4",
|
|
901
|
+
duration: 6,
|
|
902
|
+
});
|
|
903
|
+
});
|
|
904
|
+
});
|
package/src/vision/cli.mjs
CHANGED
|
@@ -395,7 +395,8 @@ function extractVideoFrames(videoPath, intervalSeconds = DEFAULT_VTT_SAMPLE_INTE
|
|
|
395
395
|
catch (e) { try { rmSync(tmpDir, { recursive: true, force: true }); } catch {} throw new Error(`Frame extraction failed: ${e.message}`); }
|
|
396
396
|
const frames = readdirSync(tmpDir).filter((f) => f.startsWith(baseName) && f.endsWith(".jpg")).sort().map((f) => join(tmpDir, f));
|
|
397
397
|
if (frames.length === 0) { try { rmSync(tmpDir, { recursive: true, force: true }); } catch {} throw new Error("No frames extracted from video"); }
|
|
398
|
-
|
|
398
|
+
const timing = frames.map((frame, i) => ({ frame, seconds: ((i + 0.5) * Math.max(duration || 5, 1)) / frames.length }));
|
|
399
|
+
return { frames, timing, cleanup: () => { try { rmSync(tmpDir, { recursive: true, force: true }); } catch {} } };
|
|
399
400
|
}
|
|
400
401
|
|
|
401
402
|
function runVTT(videoPath, promptText, vttCli, ittCli, sampleInterval = DEFAULT_VTT_SAMPLE_INTERVAL) {
|
|
@@ -416,7 +417,11 @@ function runVTT(videoPath, promptText, vttCli, ittCli, sampleInterval = DEFAULT_
|
|
|
416
417
|
let extracted;
|
|
417
418
|
try { extracted = extractVideoFrames(videoPath, sampleInterval); }
|
|
418
419
|
catch (e) { emitWarn(` ${e.message}`); return ""; }
|
|
419
|
-
const
|
|
420
|
+
const timingContext = extracted.timing
|
|
421
|
+
.map((item, i) => `Frame ${i + 1}: approximately ${item.seconds.toFixed(1)}s`)
|
|
422
|
+
.join("\n");
|
|
423
|
+
const framePrompt = `${promptText}\n\nThe attached frames are ordered chronologically. Approximate timing:\n${timingContext}`;
|
|
424
|
+
const result = runITT(extracted.frames, framePrompt, ittCli);
|
|
420
425
|
extracted.cleanup();
|
|
421
426
|
return result;
|
|
422
427
|
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { accessSync, constants, existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
const LOCAL_MODEL = process.env.MARKCUT_LOCAL_VISION_MODEL || "mlx-community/MiniCPM-V-4.6-bf16";
|
|
8
|
+
|
|
9
|
+
function parseArgs(argv) {
|
|
10
|
+
let prompt = "";
|
|
11
|
+
const inputs = [];
|
|
12
|
+
for (let i = 0; i < argv.length; i++) {
|
|
13
|
+
const arg = argv[i];
|
|
14
|
+
if (arg === "--prompt") prompt = argv[++i] || "";
|
|
15
|
+
else if (arg === "--input") inputs.push(argv[++i]);
|
|
16
|
+
else if (arg && !arg.startsWith("--")) inputs.push(arg.startsWith("@") ? arg.slice(1) : arg);
|
|
17
|
+
}
|
|
18
|
+
return { prompt, inputs: inputs.filter(Boolean) };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function which(name) {
|
|
22
|
+
if (!name) return "";
|
|
23
|
+
const candidates = name.includes("/") ? [name] : (process.env.PATH || "").split(":").filter(Boolean).map(dir => join(dir, name));
|
|
24
|
+
for (const candidate of candidates) {
|
|
25
|
+
try { accessSync(candidate, constants.X_OK); return candidate; } catch {}
|
|
26
|
+
}
|
|
27
|
+
return "";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function run(command, args, options = {}) {
|
|
31
|
+
const r = spawnSync(command, args, { encoding: "utf8", timeout: options.timeout || 600_000, env: process.env });
|
|
32
|
+
return { ok: r.status === 0, stdout: (r.stdout || "").trim(), stderr: (r.stderr || "").trim(), status: r.status };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function tryBrowser(prompt, inputs) {
|
|
36
|
+
const explicit = process.env.MARKCUT_CHATGPT_BROWSER_INFER_CLI || "";
|
|
37
|
+
const command = explicit || which("chatgpt-browser-infer");
|
|
38
|
+
if (!command) return { ok: false, reason: "browser-infer-unavailable" };
|
|
39
|
+
const args = ["--prompt", prompt];
|
|
40
|
+
for (const input of inputs) args.push("--file", input);
|
|
41
|
+
const r = run(command, args, { timeout: Number(process.env.MARKCUT_CHATGPT_VISION_TIMEOUT_MS) || 600_000 });
|
|
42
|
+
return r.ok && r.stdout ? { ok: true, output: r.stdout } : { ok: false, reason: r.stderr || `browser-infer-exit-${r.status}` };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function tryCodex(prompt, inputs) {
|
|
46
|
+
const codex = which("codex");
|
|
47
|
+
if (!codex) return { ok: false, reason: "codex-unavailable" };
|
|
48
|
+
const profiles = (process.env.MARKCUT_CODEX_VISION_PROFILES || "zai,default").split(",").map(s => s.trim()).filter(Boolean);
|
|
49
|
+
const errors = [];
|
|
50
|
+
for (const profile of profiles) {
|
|
51
|
+
const dir = mkdtempSync(join(tmpdir(), "markcut-codex-vision-"));
|
|
52
|
+
const output = join(dir, "result.txt");
|
|
53
|
+
const args = ["exec", "--ephemeral", "--skip-git-repo-check"];
|
|
54
|
+
if (profile !== "default") args.push("-p", profile);
|
|
55
|
+
if (inputs.length) args.push("-i", ...inputs);
|
|
56
|
+
args.push("-o", output, prompt);
|
|
57
|
+
const r = run(codex, args, { timeout: Number(process.env.MARKCUT_CODEX_VISION_TIMEOUT_MS) || 600_000 });
|
|
58
|
+
let text = "";
|
|
59
|
+
if (existsSync(output)) text = readFileSync(output, "utf8").trim();
|
|
60
|
+
rmSync(dir, { recursive: true, force: true });
|
|
61
|
+
if (r.ok && text) return { ok: true, output: text };
|
|
62
|
+
errors.push(`${profile}:${r.stderr || `exit-${r.status}`}`);
|
|
63
|
+
}
|
|
64
|
+
return { ok: false, reason: errors.join("; ") || "codex-failed" };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function tryLocal(prompt, inputs) {
|
|
68
|
+
const uvx = which("uvx");
|
|
69
|
+
if (!uvx) return { ok: false, reason: "uvx-unavailable" };
|
|
70
|
+
const args = ["--from", "mlx-vlm", "mlx_vlm.generate", "--model", LOCAL_MODEL, "--max-tokens", "2048", "--prompt", prompt, "--image", ...inputs, "--temperature", "0.0", "--thinking-mode", "disabled"];
|
|
71
|
+
const r = run(uvx, args, { timeout: Number(process.env.MARKCUT_LOCAL_VISION_TIMEOUT_MS) || 600_000 });
|
|
72
|
+
return r.ok && r.stdout ? { ok: true, output: r.stdout } : { ok: false, reason: r.stderr || `local-vlm-exit-${r.status}` };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function infer({ prompt, inputs }) {
|
|
76
|
+
if (!prompt) throw new Error("--prompt is required");
|
|
77
|
+
if (!inputs?.length) throw new Error("at least one image input is required");
|
|
78
|
+
|
|
79
|
+
const attempts = [
|
|
80
|
+
["browser-chatgpt", () => tryBrowser(prompt, inputs)],
|
|
81
|
+
["codex", () => tryCodex(prompt, inputs)],
|
|
82
|
+
["local-vlm", () => tryLocal(prompt, inputs)],
|
|
83
|
+
];
|
|
84
|
+
const failures = [];
|
|
85
|
+
for (const [name, fn] of attempts) {
|
|
86
|
+
const result = fn();
|
|
87
|
+
if (result.ok) return result.output;
|
|
88
|
+
failures.push(`${name}: ${result.reason}`);
|
|
89
|
+
}
|
|
90
|
+
throw new Error(`all vision backends failed (${failures.join(" | ")})`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
94
|
+
try {
|
|
95
|
+
const { prompt, inputs } = parseArgs(process.argv.slice(2));
|
|
96
|
+
process.stdout.write(infer({ prompt, inputs }) + "\n");
|
|
97
|
+
} catch (error) {
|
|
98
|
+
process.stderr.write(`${error?.message || error}\n`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
2
|
+
import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { spawnSync } from "node:child_process";
|
|
7
|
+
|
|
8
|
+
const ROOT = resolve(fileURLToPath(import.meta.url), "..", "..");
|
|
9
|
+
const HELPER = join(ROOT, "src", "vision", "default-infer.mjs");
|
|
10
|
+
const tempDirs: string[] = [];
|
|
11
|
+
|
|
12
|
+
function tempBin() {
|
|
13
|
+
const dir = mkdtempSync(join(tmpdir(), "markcut-vision-test-"));
|
|
14
|
+
tempDirs.push(dir);
|
|
15
|
+
return dir;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function script(dir: string, name: string, body: string) {
|
|
19
|
+
const path = join(dir, name);
|
|
20
|
+
writeFileSync(path, `#!/bin/sh\n${body}\n`);
|
|
21
|
+
chmodSync(path, 0o755);
|
|
22
|
+
return path;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function runHelper(bin: string) {
|
|
26
|
+
return spawnSync(process.execPath, [HELPER, "--prompt", "describe", "/tmp/a.jpg"], {
|
|
27
|
+
encoding: "utf8",
|
|
28
|
+
env: {
|
|
29
|
+
...process.env,
|
|
30
|
+
PATH: `${bin}:/bin:/usr/bin`,
|
|
31
|
+
MARKCUT_CHATGPT_BROWSER_INFER_CLI: "",
|
|
32
|
+
MARKCUT_CODEX_VISION_PROFILES: "zai,default",
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
for (const dir of tempDirs.splice(0)) rmSync(dir, { recursive: true, force: true });
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("default vision inference priority", () => {
|
|
42
|
+
it("uses Browser ChatGPT first", () => {
|
|
43
|
+
const bin = tempBin();
|
|
44
|
+
script(bin, "chatgpt-browser-infer", 'echo browser-result');
|
|
45
|
+
script(bin, "codex", 'echo codex-should-not-run >&2; exit 9');
|
|
46
|
+
script(bin, "uvx", 'echo local-should-not-run >&2; exit 9');
|
|
47
|
+
const r = runHelper(bin);
|
|
48
|
+
expect(r.status).toBe(0);
|
|
49
|
+
expect(r.stdout.trim()).toBe("browser-result");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("falls back to Codex after Browser ChatGPT failure", () => {
|
|
53
|
+
const bin = tempBin();
|
|
54
|
+
script(bin, "chatgpt-browser-infer", 'exit 2');
|
|
55
|
+
script(bin, "codex", 'out=""; while [ "$#" -gt 0 ]; do if [ "$1" = "-o" ]; then out="$2"; shift 2; else shift; fi; done; echo codex-result > "$out"');
|
|
56
|
+
script(bin, "uvx", 'echo local-should-not-run >&2; exit 9');
|
|
57
|
+
const r = runHelper(bin);
|
|
58
|
+
expect(r.status).toBe(0);
|
|
59
|
+
expect(r.stdout.trim()).toBe("codex-result");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it("uses local VLM only after Browser ChatGPT and Codex fail", () => {
|
|
63
|
+
const bin = tempBin();
|
|
64
|
+
script(bin, "chatgpt-browser-infer", 'exit 2');
|
|
65
|
+
script(bin, "codex", 'exit 3');
|
|
66
|
+
script(bin, "uvx", 'echo local-result');
|
|
67
|
+
const r = runHelper(bin);
|
|
68
|
+
expect(r.status).toBe(0);
|
|
69
|
+
expect(r.stdout.trim()).toBe("local-result");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("config defaults ITT to the policy helper and VTT to frame-based ITT", () => {
|
|
73
|
+
const code = 'import("./src/config.mjs").then(m => console.log(JSON.stringify({itt:m.DEFAULT_ITT_CLI,vtt:m.DEFAULT_VTT_CLI})))';
|
|
74
|
+
const env = { ...process.env } as Record<string, string>;
|
|
75
|
+
delete env.MARKCUT_ITT_CLI;
|
|
76
|
+
delete env.MARKCUT_VTT_CLI;
|
|
77
|
+
const r = spawnSync(process.execPath, ["--input-type=module", "-e", code], { cwd: ROOT, encoding: "utf8", env });
|
|
78
|
+
expect(r.status).toBe(0);
|
|
79
|
+
const value = JSON.parse(r.stdout.trim());
|
|
80
|
+
expect(value.itt).toContain("default-infer.mjs");
|
|
81
|
+
expect(value.vtt).toBe("");
|
|
82
|
+
});
|
|
83
|
+
});
|