@lalalic/markcut 2.2.7 → 2.2.9

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 (48) hide show
  1. package/package.json +2 -1
  2. package/skills/markcut/SKILL.md +0 -2
  3. package/skills/markcut/docs/markdown-descriptive.md +1 -1
  4. package/src/config.mjs +60 -63
  5. package/src/descriptive/resolve.ts +3 -2
  6. package/src/player/browser.tsx +3 -1
  7. package/src/player/bundle/player.js +7 -2
  8. package/src/player/components/SceneThumbnails.tsx +3 -1
  9. package/src/player/pipeline.mjs +64 -21
  10. package/src/player/server.mjs +11 -2
  11. package/src/render/cli.mjs +42 -69
  12. package/src/vision/cli.mjs +21 -45
  13. package/skills/markcut/docs/template.md +0 -212
  14. package/skills/markcut/docs/templates/courseware/TEMPLATE.md +0 -526
  15. package/skills/markcut/docs/templates/courseware/agents/reviewer.md +0 -84
  16. package/skills/markcut/docs/templates/courseware/prompts/fix.md +0 -30
  17. package/skills/markcut/docs/templates/courseware/prompts/outline.md +0 -58
  18. package/skills/markcut/docs/templates/courseware/prompts/scene.md +0 -70
  19. package/skills/markcut/docs/templates/deep-dive/TEMPLATE.md +0 -354
  20. package/skills/markcut/docs/templates/deep-dive/agents/researcher.md +0 -69
  21. package/skills/markcut/docs/templates/deep-dive/agents/reviewer.md +0 -108
  22. package/skills/markcut/docs/templates/deep-dive/prompts/fix.md +0 -28
  23. package/skills/markcut/docs/templates/deep-dive/prompts/outline.md +0 -102
  24. package/skills/markcut/docs/templates/deep-dive/prompts/script.md +0 -64
  25. package/skills/markcut/docs/templates/illustrated-book/TEMPLATE.md +0 -356
  26. package/skills/markcut/docs/templates/illustrated-book/agents/reviewer.md +0 -115
  27. package/skills/markcut/docs/templates/illustrated-book/prompts/fix.md +0 -29
  28. package/skills/markcut/docs/templates/illustrated-book/prompts/illustration.md +0 -35
  29. package/skills/markcut/docs/templates/illustrated-book/prompts/story.md +0 -69
  30. package/skills/markcut/docs/templates/short-film/TEMPLATE.md +0 -385
  31. package/skills/markcut/docs/templates/short-film/agents/reviewer.md +0 -115
  32. package/skills/markcut/docs/templates/short-film/prompts/fix.md +0 -29
  33. package/skills/markcut/docs/templates/short-film/prompts/screenplay.md +0 -101
  34. package/skills/markcut/docs/templates/short-film/prompts/storyboard.md +0 -54
  35. package/skills/markcut/docs/templates/short-video/TEMPLATE.md +0 -301
  36. package/skills/markcut/docs/templates/short-video/agents/reviewer.md +0 -94
  37. package/skills/markcut/docs/templates/short-video/prompts/fix.md +0 -27
  38. package/skills/markcut/docs/templates/short-video/prompts/script.md +0 -65
  39. package/skills/markcut/docs/templates/vlog/TEMPLATE.md +0 -326
  40. package/skills/markcut/docs/templates/vlog/agents/reviewer.md +0 -89
  41. package/skills/markcut/docs/templates/vlog/agents/story-writer.md +0 -100
  42. package/skills/markcut/docs/templates/vlog/prompts/bgm-select.md +0 -38
  43. package/skills/markcut/docs/templates/vlog/prompts/group-clips.md +0 -93
  44. package/skills/markcut/docs/templates/vlog/prompts/local-context.md +0 -59
  45. package/skills/markcut/docs/templates/vlog/prompts/outline.md +0 -56
  46. package/skills/markcut/docs/templates/vlog/prompts/review-fix.md +0 -27
  47. package/skills/markcut/docs/templates/vlog/prompts/storyboard.md +0 -41
  48. package/src/render/cli.test.ts +0 -13
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lalalic/markcut",
3
- "version": "2.2.7",
3
+ "version": "2.2.9",
4
4
  "description": "Markdown-to-video engine. Describe scenes in markdown, get a rendered video.",
5
5
  "bin": {
6
6
  "markcut": "bin/markcut"
@@ -31,6 +31,7 @@
31
31
  "test:watch": "vitest --config tests/vitest.config.ts",
32
32
  "build": "scripts/build-pipeline.sh && scripts/build-player.sh",
33
33
  "vision": "node src/render/cli.mjs vision",
34
+ "markcut": "bin/markcut",
34
35
  "push": "GIT_SSH_COMMAND=\"ssh -i ~/.ssh/id_ed25519_1 -o IdentitiesOnly=yes\" git push"
35
36
  },
36
37
  "dependencies": {
@@ -62,8 +62,6 @@ some common issues (photo or video can't be displayed, audio missing), take belo
62
62
  | Markdown descriptive format (primary authoring format) | [docs/markdown-descriptive.md](docs/markdown-descriptive.md) |
63
63
  | Label system (browse, label, export labels.json) | [docs/label-mode.md](docs/label-mode.md) |
64
64
  | Player servers (label + edit mode) | [docs/edit-mode.md](docs/edit-mode.md) |
65
- | How to make video template | [docs/template.md](docs/template.md) |
66
- | Templates | [docs/templates/](docs/templates/) |
67
65
 
68
66
 
69
67
  ## common used components
@@ -529,7 +529,7 @@ layout:series width:1920 height:1080 fps:30
529
529
  tts:"edge-tts --voice 'en-US-GuyNeural' --text '{input}' --write-media '{output}'"
530
530
 
531
531
  ## Hook
532
- - image duration:3 prompt:"..."
532
+ - image prompt:"..."
533
533
 
534
534
  ## Title
535
535
  - script "Welcome to the course"
package/src/config.mjs CHANGED
@@ -8,6 +8,48 @@
8
8
  * @module
9
9
  */
10
10
 
11
+
12
+ export const args=(function parseArgs(argv) {
13
+ const CLI_OVERRIDE_FLAGS = {
14
+ "--itt": "itt",
15
+ "--vtt": "vtt",
16
+ "--stt": "stt",
17
+ "--tts": "tts",
18
+ "--agent": "agent",
19
+ "--edit-cli": "editCli",
20
+ "--tti": "tti",
21
+ "--ttv": "ttv"
22
+ };
23
+ const args = { command: "", file: "", output: "", forceNew: false, verbose: false, label: false, edit: false, noBrowser: false, chat: false, port: 3001, compile: false, cli: false, showClis: false, scriptOutputDir: "", mediaOutputDir: "", variant: [], cliOverrides: {} };
24
+ let i = 2;
25
+ if (argv[i]) args.command = argv[i++];
26
+ if (argv[i] && !argv[i].startsWith("--")) args.file = argv[i++];
27
+ while (i < argv.length) {
28
+ const flag = argv[i++];
29
+ if (flag === "--output" && argv[i]) args.output = argv[i++];
30
+ else if (flag === "--script-output-dir" && argv[i]) args.scriptOutputDir = argv[i++];
31
+ else if (flag === "--media-output-dir" && argv[i]) args.mediaOutputDir = argv[i++];
32
+ else if (flag === "--cli") args.cli = true;
33
+ else if (flag === "--show-clis") args.showClis = true;
34
+ else if (flag === "--compile") args.compile = true;
35
+ else if (flag === "--force-new") args.forceNew = true;
36
+ else if (flag === "--verbose") args.verbose = true;
37
+ else if (flag === "--label") args.label = true;
38
+ else if (flag === "--edit") args.edit = true;
39
+ else if (flag === "--no-browser") args.noBrowser = true;
40
+ else if (flag === "--port" && argv[i]) args.port = parseInt(argv[i], 10);
41
+ else if (flag.startsWith("--port=")) args.port = parseInt(flag.split("=")[1], 10);
42
+ else if (flag === "--variant" && argv[i]) args.variant.push(argv[i++]);
43
+ else if (flag.startsWith("--variant=")) args.variant.push(flag.split("=")[1]);
44
+ else if (CLI_OVERRIDE_FLAGS[flag] && argv[i]) {
45
+ const key = CLI_OVERRIDE_FLAGS[flag];
46
+ args.cliOverrides[key] = argv[i++];
47
+ }
48
+ }
49
+ return args;
50
+ })(process.argv);
51
+
52
+
11
53
  // ── Media type sets ───────────────────────────────────────────────────────
12
54
 
13
55
  export const IMAGE_EXTS = new Set([".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".tiff", ".heic", ".avif"]);
@@ -25,14 +67,8 @@ export const MAX_VIDEO_DIMENSION = Number(process.env.MARKCUT_MAX_VIDEO_DIMENSIO
25
67
  // ── Vision pipeline CLI templates ─────────────────────────────────────────
26
68
 
27
69
  /**
28
- * Image-to-text (ITT) CLI.
29
- * Reads an image file and returns a text description.
30
- * Placeholders: {input}=image path(s), {prompt}=description prompt.
31
- * Override with MARKCUT_ITT_CLI env var.
70
+ * Image-to-text (ITT) CLI placeholder (now defined below via export let).
32
71
  */
33
- export const DEFAULT_ITT_CLI =
34
- process.env.MARKCUT_ITT_CLI ||
35
- 'uvx --from mlx-vlm mlx_vlm.generate --model mlx-community/MiniCPM-V-4.6-bf16 --max-tokens 2048 --prompt "{prompt}" --image {input} --temperature 0.0 --thinking-mode disabled';
36
72
 
37
73
  /**
38
74
  * Seconds between sampled video frames when using the default ITT-based VTT.
@@ -42,62 +78,23 @@ export const GOOGLE_MAPS_API_KEY = process.env.GOOGLE_MAPS_API_KEY || "";
42
78
  export const DEFAULT_VTT_SAMPLE_INTERVAL = Number(process.env.MARKCUT_VTT_SAMPLE_INTERVAL) || 5;
43
79
 
44
80
  /**
45
- * Video-to-text (VTT) CLI direct video model.
46
- * When set to null/empty, falls back to ITT via frame extraction.
47
- * Placeholders: {input}=video path, {prompt}=description prompt.
48
- * Override with MARKCUT_VTT_CLI env var.
49
- */
50
- export const DEFAULT_VTT_CLI =
51
- process.env.MARKCUT_VTT_CLI ||
52
- 'uvx --from mlx-vlm mlx_vlm.generate --model mlx-community/MiniCPM-V-4.6-bf16 --max-tokens 2048 --prompt "{prompt}" --video {input} --temperature 0.0 --processor-kwargs \'{"max_num_frames": 32, "stack_frames": 1, "max_slice_nums": 1, "use_image_id": false}\'';
53
-
54
- /**
55
- * Speech-to-text (STT) CLI.
56
- * Extracts audio and generates VTT subtitles.
57
- * Placeholders: {input}=audio path, {output}=output directory.
58
- * Override with MARKCUT_STT_CLI env var.
59
- * Used by both vision and render pipelines.
60
- */
61
- export const DEFAULT_STT_CLI =
62
- process.env.MARKCUT_STT_CLI ||
63
- 'uvx --from openai-whisper whisper "{input}" --output_format vtt --output_dir "{output}"';
64
-
65
- /**
66
- * Text-to-speech (TTS) CLI.
67
- * Generates audio from text.
68
- * Placeholders: {input}=text, {output}=audio file path.
69
- * Override with MARKCUT_TTS_CLI env var.
70
- * Used by both vision and render pipelines.
71
- */
72
- export const DEFAULT_TTS_CLI =
73
- process.env.MARKCUT_TTS_CLI ||
74
- 'uvx edge-tts --voice "en-US-GuyNeural" --text "{input}" --write-media "{output}"';
75
-
76
- /**
77
- * Default agent CLI — general-purpose text LLM.
78
- * Used for text-only tasks like detect-scenes.
79
- * Placeholder:
80
- * {prompt} = user prompt (current context + edit request)
81
- * Override with MARKCUT_AGENT_CLI env var.
82
- */
83
- export const DEFAULT_AGENT_CLI = process.env.MARKCUT_AGENT_CLI || 'npx pi -p {prompt}';
84
-
85
- /**
86
- * Default edit agent CLI — used by the edit-mode persistent rpc session.
87
- * Placeholders:
88
- * {systemprompt} = system prompt (role, instructions, knowledge)
89
- * {prompt} = user prompt (current context + edit request)
90
- * {sessionid} = unique session ID for conversation continuity
91
- * Override with MARKCUT_EDIT_CLI env var.
81
+ * Video-to-text (VTT) CLI placeholder (now defined below via export let).
92
82
  */
93
- export const DEFAULT_EDIT_CLI = process.env.MARKCUT_EDIT_CLI || 'npx pi --session-id {sessionid} --system-prompt {systemprompt} -p {prompt}';
94
83
 
95
- // ── Render-only pipeline CLI templates ─────────────────────────────────────
96
- // These are specific to the render pipeline (no vision pipeline equivalent).
97
- // All templates support {input}, {output}, and {seed} placeholders.
98
- // {seed} is substituted with root.seed (or empty string if not set).
99
84
 
100
- export const DEFAULT_TTI_CLI =
101
- process.env.MARKCUT_TTI_CLI ||
102
- 'uvx --from mflux mflux-generate-flux2 --model flux2-klein-4b --steps 5 --prompt "{input}" --output "{output}" --seed {seed}';
103
- export const DEFAULT_TTV_CLI = process.env.MARKCUT_TTV_CLI || '';
85
+ /** Speech-to-text CLI. Override via --stt flag or MARKCUT_STT_CLI env var. */
86
+ export const DEFAULT_STT_CLI = args.cliOverrides.stt || process.env.MARKCUT_STT_CLI || 'uvx --from openai-whisper whisper "{input}" --output_format vtt --output_dir "{output}"';
87
+ /** Text-to-speech CLI. Override via --tts flag or MARKCUT_TTS_CLI env var. */
88
+ export const DEFAULT_TTS_CLI = args.cliOverrides.tts || process.env.MARKCUT_TTS_CLI || 'uvx edge-tts --voice "en-US-GuyNeural" --text "{input}" --write-media "{output}"';
89
+ /** Default agent CLI. Override via --agent flag or MARKCUT_AGENT_CLI env var. */
90
+ export const DEFAULT_AGENT_CLI = args.cliOverrides.agent || process.env.MARKCUT_AGENT_CLI || 'npx pi -p {prompt}';
91
+ /** Default edit agent CLI. Override via --edit-cli flag or MARKCUT_EDIT_CLI env var. */
92
+ export const DEFAULT_EDIT_CLI = args.cliOverrides.editCli || process.env.MARKCUT_EDIT_CLI || 'npx pi --session-id {sessionid} --system-prompt {systemprompt} -p {prompt}';
93
+ /** Text-to-image CLI. Override via --tti flag or MARKCUT_TTI_CLI env var. */
94
+ export const DEFAULT_TTI_CLI = args.cliOverrides.tti || process.env.MARKCUT_TTI_CLI || 'uvx --from mflux mflux-generate-flux2 --model flux2-klein-4b --steps 5 --prompt "{input}" --output "{output}" --seed {seed}';
95
+ /** Text-to-video CLI. Override via --ttv flag or MARKCUT_TTV_CLI env var. */
96
+ export const DEFAULT_TTV_CLI = args.cliOverrides.ttv || process.env.MARKCUT_TTV_CLI || '';
97
+ /** Image-to-text CLI. Override via --itt flag or MARKCUT_ITT_CLI env var. */
98
+ export const DEFAULT_ITT_CLI = args.cliOverrides.itt || process.env.MARKCUT_ITT_CLI || 'uvx --from mlx-vlm mlx_vlm.generate --model mlx-community/MiniCPM-V-4.6-bf16 --max-tokens 2048 --prompt "{prompt}" --image {input} --temperature 0.0 --thinking-mode disabled';;
99
+ /** Video-to-text CLI. Override via --vtt flag or MARKCUT_VTT_CLI env var. */
100
+ export const DEFAULT_VTT_CLI = args.cliOverrides.vtt || process.env.MARKCUT_VTT_CLI || 'uvx --from mlx-vlm mlx_vlm.generate --model mlx-community/MiniCPM-V-4.6-bf16 --max-tokens 2048 --prompt "{prompt}" --video {input} --temperature 0.0 --processor-kwargs \'{"max_num_frames": 32, "stack_frames": 1, "max_slice_nums": 1, "use_image_id": false}\'';
@@ -713,8 +713,9 @@ export async function resolveGeneratedMedia(
713
713
  walkDown(clone as any, (node) => {
714
714
  if ((node.type !== "image" && node.type !== "video")) return;
715
715
  if (!node.prompt || typeof node.prompt !== "string") return;
716
- // Skip if src is already set (prompt is just metadata)
717
- if (node.src) return;
716
+ // Skip if src is already set to a real path (prompt is just metadata).
717
+ // "auto" is a placeholder meaning "auto-generate via TTI/TTV".
718
+ if (node.src && node.src !== "auto") return;
718
719
  const id = node.id ?? `${node.type}-${genNodes.length}`;
719
720
  genNodes.push({ node, id, type: node.type as "image" | "video", prompt: node.prompt });
720
721
  });
@@ -123,7 +123,9 @@ function PlayerApp() {
123
123
 
124
124
  // Fetch scenes for active scene tracking
125
125
  React.useEffect(() => {
126
- fetch("/api/video-info")
126
+ const variant = (window as any).VARIANT || "default";
127
+ const url = variant !== "default" ? `/api/video-info?variant=${variant}` : "/api/video-info";
128
+ fetch(url)
127
129
  .then((r) => r.json())
128
130
  .then((info) => {
129
131
  if (info.scenes) {
@@ -59780,6 +59780,7 @@ var root = folder.extend({
59780
59780
  fps: external_exports.number().int().positive().default(30),
59781
59781
  instruction: external_exports.string().optional(),
59782
59782
  metadata: external_exports.string().optional(),
59783
+ seed: external_exports.number().optional().describe("global seed for TTI/TTV generation; substituted into CLI {seed} placeholder"),
59783
59784
  stylesheet: external_exports.string().optional().describe("global css; selectors use .type and .name"),
59784
59785
  subtitle: subtitleOverlay.optional().describe("global subtitle overlay; src is a VTT file with absolute timestamps"),
59785
59786
  /**
@@ -60351,7 +60352,9 @@ var import_jsx_runtime93 = __toESM(require_jsx_runtime(), 1);
60351
60352
  function SceneThumbnails({ currentTime, onSeek }) {
60352
60353
  const [scenes, setScenes] = React51.useState([]);
60353
60354
  React51.useEffect(() => {
60354
- fetch("/api/video-info").then((r) => r.json()).then((info2) => {
60355
+ const variant = window.VARIANT || "default";
60356
+ const url2 = variant !== "default" ? `/api/video-info?variant=${variant}` : "/api/video-info";
60357
+ fetch(url2).then((r) => r.json()).then((info2) => {
60355
60358
  if (info2.scenes) setScenes(info2.scenes);
60356
60359
  }).catch(() => {
60357
60360
  });
@@ -60482,7 +60485,9 @@ function PlayerApp() {
60482
60485
  const [currentTime, setCurrentTime] = React55.useState(0);
60483
60486
  const [activeScene, setActiveScene] = React55.useState("");
60484
60487
  React55.useEffect(() => {
60485
- fetch("/api/video-info").then((r) => r.json()).then((info2) => {
60488
+ const variant = window.VARIANT || "default";
60489
+ const url2 = variant !== "default" ? `/api/video-info?variant=${variant}` : "/api/video-info";
60490
+ fetch(url2).then((r) => r.json()).then((info2) => {
60486
60491
  if (info2.scenes) {
60487
60492
  window.__scenes = info2.scenes;
60488
60493
  }
@@ -23,7 +23,9 @@ export function SceneThumbnails({ currentTime, onSeek }: SceneThumbnailsProps) {
23
23
  const [scenes, setScenes] = React.useState<Scene[]>([]);
24
24
 
25
25
  React.useEffect(() => {
26
- fetch("/api/video-info")
26
+ const variant = (window as any).VARIANT || "default";
27
+ const url = variant !== "default" ? `/api/video-info?variant=${variant}` : "/api/video-info";
28
+ fetch(url)
27
29
  .then((r) => r.json())
28
30
  .then((info) => {
29
31
  if (info.scenes) setScenes(info.scenes);
@@ -883,8 +883,8 @@ var require_format = __commonJS({
883
883
  return format.apply(null, [fmt].concat(replacements));
884
884
  }
885
885
  function format(fmt) {
886
- var argIndex = 1, args = [].slice.call(arguments), i = 0, n = fmt.length, result = "", c, escaped = false, arg, tmp, leadingZero = false, precision, nextArg = function() {
887
- return args[argIndex++];
886
+ var argIndex = 1, args2 = [].slice.call(arguments), i = 0, n = fmt.length, result = "", c, escaped = false, arg, tmp, leadingZero = false, precision, nextArg = function() {
887
+ return args2[argIndex++];
888
888
  }, slurpNumber = function() {
889
889
  var digits = "";
890
890
  while (/\d/.test(fmt[i])) {
@@ -972,22 +972,61 @@ import { existsSync, mkdirSync } from "node:fs";
972
972
  import { dirname } from "node:path";
973
973
 
974
974
  // src/config.mjs
975
+ var args = (function parseArgs(argv) {
976
+ const CLI_OVERRIDE_FLAGS = {
977
+ "--itt": "itt",
978
+ "--vtt": "vtt",
979
+ "--stt": "stt",
980
+ "--tts": "tts",
981
+ "--agent": "agent",
982
+ "--edit-cli": "editCli",
983
+ "--tti": "tti",
984
+ "--ttv": "ttv"
985
+ };
986
+ const args2 = { command: "", file: "", output: "", forceNew: false, verbose: false, label: false, edit: false, noBrowser: false, chat: false, port: 3001, compile: false, cli: false, showClis: false, scriptOutputDir: "", mediaOutputDir: "", variant: [], cliOverrides: {} };
987
+ let i = 2;
988
+ if (argv[i]) args2.command = argv[i++];
989
+ if (argv[i] && !argv[i].startsWith("--")) args2.file = argv[i++];
990
+ while (i < argv.length) {
991
+ const flag = argv[i++];
992
+ if (flag === "--output" && argv[i]) args2.output = argv[i++];
993
+ else if (flag === "--script-output-dir" && argv[i]) args2.scriptOutputDir = argv[i++];
994
+ else if (flag === "--media-output-dir" && argv[i]) args2.mediaOutputDir = argv[i++];
995
+ else if (flag === "--cli") args2.cli = true;
996
+ else if (flag === "--show-clis") args2.showClis = true;
997
+ else if (flag === "--compile") args2.compile = true;
998
+ else if (flag === "--force-new") args2.forceNew = true;
999
+ else if (flag === "--verbose") args2.verbose = true;
1000
+ else if (flag === "--label") args2.label = true;
1001
+ else if (flag === "--edit") args2.edit = true;
1002
+ else if (flag === "--no-browser") args2.noBrowser = true;
1003
+ else if (flag === "--port" && argv[i]) args2.port = parseInt(argv[i], 10);
1004
+ else if (flag.startsWith("--port=")) args2.port = parseInt(flag.split("=")[1], 10);
1005
+ else if (flag === "--variant" && argv[i]) args2.variant.push(argv[i++]);
1006
+ else if (flag.startsWith("--variant=")) args2.variant.push(flag.split("=")[1]);
1007
+ else if (CLI_OVERRIDE_FLAGS[flag] && argv[i]) {
1008
+ const key = CLI_OVERRIDE_FLAGS[flag];
1009
+ args2.cliOverrides[key] = argv[i++];
1010
+ }
1011
+ }
1012
+ return args2;
1013
+ })(process.argv);
975
1014
  var MAX_IMAGE_DIMENSION = Number(process.env.MARKCUT_MAX_IMAGE_DIMENSION) || 384;
976
1015
  var MAX_VIDEO_DURATION = Number(process.env.MARKCUT_MAX_VIDEO_DURATION) || 60;
977
1016
  var MAX_VIDEO_DIMENSION = Number(process.env.MARKCUT_MAX_VIDEO_DIMENSION) || 360;
978
- var DEFAULT_ITT_CLI = process.env.MARKCUT_ITT_CLI || 'uvx --from mlx-vlm mlx_vlm.generate --model mlx-community/MiniCPM-V-4.6-bf16 --max-tokens 2048 --prompt "{prompt}" --image {input} --temperature 0.0 --thinking-mode disabled';
979
1017
  var GOOGLE_MAPS_API_KEY = process.env.GOOGLE_MAPS_API_KEY || "";
980
1018
  var DEFAULT_VTT_SAMPLE_INTERVAL = Number(process.env.MARKCUT_VTT_SAMPLE_INTERVAL) || 5;
981
- var DEFAULT_VTT_CLI = process.env.MARKCUT_VTT_CLI || `uvx --from mlx-vlm mlx_vlm.generate --model mlx-community/MiniCPM-V-4.6-bf16 --max-tokens 2048 --prompt "{prompt}" --video {input} --temperature 0.0 --processor-kwargs '{"max_num_frames": 32, "stack_frames": 1, "max_slice_nums": 1, "use_image_id": false}'`;
982
- var DEFAULT_STT_CLI = process.env.MARKCUT_STT_CLI || 'uvx --from openai-whisper whisper "{input}" --output_format vtt --output_dir "{output}"';
983
- var DEFAULT_TTS_CLI = process.env.MARKCUT_TTS_CLI || 'uvx edge-tts --voice "en-US-GuyNeural" --text "{input}" --write-media "{output}"';
984
- var DEFAULT_AGENT_CLI = process.env.MARKCUT_AGENT_CLI || "npx pi -p {prompt}";
985
- var DEFAULT_EDIT_CLI = process.env.MARKCUT_EDIT_CLI || "npx pi --session-id {sessionid} --system-prompt {systemprompt} -p {prompt}";
986
- var DEFAULT_TTI_CLI = process.env.MARKCUT_TTI_CLI || 'uvx --from mflux mflux-generate-flux2 --model flux2-klein-4b --steps 5 --prompt "{input}" --output "{output}"';
987
- var DEFAULT_TTV_CLI = process.env.MARKCUT_TTV_CLI || "";
1019
+ var DEFAULT_STT_CLI = args.cliOverrides.stt || process.env.MARKCUT_STT_CLI || 'uvx --from openai-whisper whisper "{input}" --output_format vtt --output_dir "{output}"';
1020
+ var DEFAULT_TTS_CLI = args.cliOverrides.tts || process.env.MARKCUT_TTS_CLI || 'uvx edge-tts --voice "en-US-GuyNeural" --text "{input}" --write-media "{output}"';
1021
+ var DEFAULT_AGENT_CLI = args.cliOverrides.agent || process.env.MARKCUT_AGENT_CLI || "npx pi -p {prompt}";
1022
+ var DEFAULT_EDIT_CLI = args.cliOverrides.editCli || process.env.MARKCUT_EDIT_CLI || "npx pi --session-id {sessionid} --system-prompt {systemprompt} -p {prompt}";
1023
+ var DEFAULT_TTI_CLI = args.cliOverrides.tti || process.env.MARKCUT_TTI_CLI || 'uvx --from mflux mflux-generate-flux2 --model flux2-klein-4b --steps 5 --prompt "{input}" --output "{output}" --seed {seed}';
1024
+ var DEFAULT_TTV_CLI = args.cliOverrides.ttv || process.env.MARKCUT_TTV_CLI || "";
1025
+ var DEFAULT_ITT_CLI = args.cliOverrides.itt || process.env.MARKCUT_ITT_CLI || 'uvx --from mlx-vlm mlx_vlm.generate --model mlx-community/MiniCPM-V-4.6-bf16 --max-tokens 2048 --prompt "{prompt}" --image {input} --temperature 0.0 --thinking-mode disabled';
1026
+ var DEFAULT_VTT_CLI = args.cliOverrides.vtt || process.env.MARKCUT_VTT_CLI || `uvx --from mlx-vlm mlx_vlm.generate --model mlx-community/MiniCPM-V-4.6-bf16 --max-tokens 2048 --prompt "{prompt}" --video {input} --temperature 0.0 --processor-kwargs '{"max_num_frames": 32, "stack_frames": 1, "max_slice_nums": 1, "use_image_id": false}'`;
988
1027
 
989
1028
  // src/render/cli-tools.ts
990
- function substituteCli(template, input, output) {
1029
+ function substituteCli(template, input, output, seed) {
991
1030
  const safeOutput = output;
992
1031
  let safeInput;
993
1032
  const inputMatch = template.match(/(['"])\{input\}/);
@@ -1001,7 +1040,8 @@ function substituteCli(template, input, output) {
1001
1040
  } else {
1002
1041
  safeInput = input.replace(/"/g, '\\"').replace(/'/g, "'\\''");
1003
1042
  }
1004
- return template.replace(/\{input\}/g, safeInput).replace(/\{output\}/g, safeOutput);
1043
+ const safeSeed = seed !== void 0 ? String(seed) : "";
1044
+ return template.replace(/\{input\}/g, safeInput).replace(/\{output\}/g, safeOutput).replace(/\{seed\}/g, safeSeed);
1005
1045
  }
1006
1046
  function generateTTS(text3, outputPath, cli) {
1007
1047
  mkdirSync(dirname(outputPath), { recursive: true });
@@ -1022,9 +1062,9 @@ async function generateSTT(audioPath, outputDir, cli) {
1022
1062
  console.warn(` \u26A0 STT failed: ${e.message}`);
1023
1063
  }
1024
1064
  }
1025
- function generateTTI(prompt, outputPath, cli) {
1065
+ function generateTTI(prompt, outputPath, cli, seed) {
1026
1066
  mkdirSync(dirname(outputPath), { recursive: true });
1027
- const cmd = substituteCli(cli ?? DEFAULT_TTI_CLI, prompt, outputPath);
1067
+ const cmd = substituteCli(cli ?? DEFAULT_TTI_CLI, prompt, outputPath, seed);
1028
1068
  try {
1029
1069
  execSync(cmd, {
1030
1070
  encoding: "utf-8",
@@ -1038,11 +1078,11 @@ function generateTTI(prompt, outputPath, cli) {
1038
1078
  }
1039
1079
  return existsSync(outputPath) ? outputPath : "";
1040
1080
  }
1041
- function generateTTV(prompt, outputPath, cli, ttiCmd) {
1081
+ function generateTTV(prompt, outputPath, cli, ttiCmd, seed) {
1042
1082
  mkdirSync(dirname(outputPath), { recursive: true });
1043
1083
  if (!cli) {
1044
1084
  const pngPath = outputPath.replace(/\.mp4$/, ".png");
1045
- const imageResult = generateTTI(prompt, pngPath, ttiCmd);
1085
+ const imageResult = generateTTI(prompt, pngPath, ttiCmd, seed);
1046
1086
  if (!imageResult || !existsSync(imageResult)) {
1047
1087
  console.error(` \u2717 TTV: TTI step produced no image for "${prompt.slice(0, 50)}..."`);
1048
1088
  return "";
@@ -1062,7 +1102,7 @@ function generateTTV(prompt, outputPath, cli, ttiCmd) {
1062
1102
  }
1063
1103
  return existsSync(outputPath) ? outputPath : "";
1064
1104
  }
1065
- const cmd = substituteCli(cli, prompt, outputPath);
1105
+ const cmd = substituteCli(cli, prompt, outputPath, seed);
1066
1106
  try {
1067
1107
  execSync(cmd, {
1068
1108
  encoding: "utf-8",
@@ -10977,14 +11017,15 @@ async function resolveGeneratedMedia(root, options) {
10977
11017
  walkDown(clone, (node2) => {
10978
11018
  if (node2.type !== "image" && node2.type !== "video") return;
10979
11019
  if (!node2.prompt || typeof node2.prompt !== "string") return;
10980
- if (node2.src) return;
11020
+ if (node2.src && node2.src !== "auto") return;
10981
11021
  const id = node2.id ?? `${node2.type}-${genNodes.length}`;
10982
11022
  genNodes.push({ node: node2, id, type: node2.type, prompt: node2.prompt });
10983
11023
  });
10984
11024
  for (const { node: node2, id, type, prompt } of genNodes) {
10985
11025
  const ext = type === "image" ? "png" : "mp4";
10986
11026
  const cli = type === "image" ? clone.tti ?? options.ttiCli ?? DEFAULT_TTI_CLI : clone.ttv ?? options.ttvCli ?? DEFAULT_TTV_CLI;
10987
- const cacheKey = computeCacheKey({ prompt, cli, type });
11027
+ const seed = options.seed;
11028
+ const cacheKey = computeCacheKey({ prompt, cli, type, seed });
10988
11029
  const outputPath = join(options.outputDir, `${cacheKey}.${ext}`);
10989
11030
  const cached = checkCache(cache, `gen:${cacheKey}`, cacheKey);
10990
11031
  const label = type === "image" ? "TTI" : "TTV";
@@ -10997,7 +11038,7 @@ async function resolveGeneratedMedia(root, options) {
10997
11038
  try {
10998
11039
  console.log(` \u{1F50A} ${label}: ${labelText}...`);
10999
11040
  const ttiCmd = clone.tti ?? options.ttiCli ?? DEFAULT_TTI_CLI;
11000
- const result = type === "image" ? generateTTI(prompt, outputPath, cli) : generateTTV(prompt, outputPath, cli, ttiCmd);
11041
+ const result = type === "image" ? generateTTI(prompt, outputPath, cli, seed) : generateTTV(prompt, outputPath, cli, ttiCmd, seed);
11001
11042
  if (result) {
11002
11043
  node2.src = outputPath;
11003
11044
  updateCache(cache, `gen:${cacheKey}`, cacheKey, outputPath);
@@ -11107,11 +11148,13 @@ async function resolveAll2(root, options = {}) {
11107
11148
  variant: options.variants?.[0] ?? "video"
11108
11149
  });
11109
11150
  result = await resolveMediaSrcs(result, { baseDir: options.baseDir });
11151
+ const generationSeed = result.seed ?? options.seed;
11110
11152
  if (options.mediaOutputDir) {
11111
11153
  result = await resolveGeneratedMedia(result, {
11112
11154
  outputDir: options.mediaOutputDir,
11113
11155
  ttiCli: options.ttiCli,
11114
- ttvCli: options.ttvCli
11156
+ ttvCli: options.ttvCli,
11157
+ seed: generationSeed
11115
11158
  });
11116
11159
  }
11117
11160
  result = await resolveMediaDurations(result, {
@@ -181,7 +181,7 @@ function startAgentProcess() {
181
181
  const cmd = resolvedParts[0];
182
182
  const cmdArgs = resolvedParts.slice(1);
183
183
 
184
- console.log(` 🎬 starting persistent agent (rpc): ${cmd} ${cmdArgs.slice(0, -1).join(" ")} --system-prompt <${systemPrompt.length} chars>`);
184
+ console.log(` 🎬 starting edit agent`);
185
185
 
186
186
  const child = spawn(cmd, cmdArgs, { cwd: ROOT, stdio: ["pipe", "pipe", "pipe"] });
187
187
  agentProcess = child;
@@ -1055,8 +1055,17 @@ Edit request: ${text}`;
1055
1055
 
1056
1056
  // Serve the main HTML page (variant-aware)
1057
1057
  if (path === "/" || path === "/index.html") {
1058
+ // If the resolved variant isn't compiled, fall back to the first
1059
+ // available variant. This handles the common case where the user
1060
+ // passes only --variant zh (no "default" variant exists) and opens
1061
+ // the root URL — the player will fetch the zh variant data instead
1062
+ // of failing on a non-existent "default" variant.
1063
+ let htmlVariant = variantLabel;
1064
+ if (!compiledRootCache.has(variantLabel) && VARIANT_CONFIGS.length > 0) {
1065
+ htmlVariant = VARIANT_CONFIGS[0].label;
1066
+ }
1058
1067
  res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1059
- res.end(getHtml(variantLabel));
1068
+ res.end(getHtml(htmlVariant));
1060
1069
  return;
1061
1070
  }
1062
1071
 
@@ -11,6 +11,11 @@ import { readFileSync, writeFileSync, mkdirSync, existsSync, copyFileSync } from
11
11
  import { createHash } from "node:crypto";
12
12
  import { resolve, dirname, join, extname } from "node:path";
13
13
  import { fileURLToPath } from "node:url";
14
+ import {
15
+ DEFAULT_TTS_CLI, DEFAULT_STT_CLI, DEFAULT_TTI_CLI, DEFAULT_TTV_CLI,
16
+ DEFAULT_ITT_CLI, DEFAULT_VTT_CLI, DEFAULT_AGENT_CLI, DEFAULT_EDIT_CLI,
17
+ args,
18
+ } from "../config.mjs";
14
19
 
15
20
  const __filename = fileURLToPath(import.meta.url);
16
21
  const __dirname = dirname(__filename);
@@ -44,13 +49,27 @@ function usage() {
44
49
  console.log(`
45
50
  markcut CLI — Markdown/JSON → video pipeline
46
51
 
52
+ npx @lalalic/markcut <command> [options]
53
+
54
+ global options:
55
+ --show-clis Print all default CLI templates and exit
56
+
57
+ --tti <template> Override default TTI CLI template
58
+ --tts <template> Override default TTS CLI template
59
+ --stt <template> Override default STT CLI template
60
+ --ttv <template> Override default TTV CLI template
61
+ --itt <template> Override default ITT CLI template
62
+ --vtt <template> Override default VTT CLI template
63
+ --agent <template> Override default agent CLI template
64
+ --edit-cli <template> Override default edit agent CLI template
65
+
66
+ --help Show this help
67
+
47
68
  Commands:
48
69
 
49
70
  verify <file> Parse + validate descriptive file
50
- --cli Check required CLI tools are installed
51
71
 
52
72
  preview <file.json|.md> Open player with live preview
53
- --edit Auto-reload on file change
54
73
  --label Open label input overlay
55
74
  --no-browser Skip opening browser automatically
56
75
  --port <num> Port for the player server (default: 3001)
@@ -61,47 +80,16 @@ Commands:
61
80
 
62
81
  vision <folder> Extract metadata into metadata.json
63
82
  --label Full pipeline: preview → label → normalize → percept → segments
64
- --agent <template> Custom text LLM CLI for detect-scenes ({prompt})
65
- --itt <template> Custom ITT CLI template with {input}, {prompt}
66
- --vtt <template> Custom VTT CLI template with {input}, {prompt}
67
- --stt <template> Custom STT CLI template with {input}, {output}
68
83
  --prompts-file <path> Path to prompts markdown file (default: vision_prompts.md)
69
84
  --vtt-sample-interval <n> Sample one video frame every N seconds (default: 5)
70
85
  --context "text" Background context about people/places (injected into prompts)
71
- --pick <files> Comma-separated filenames to process
72
86
  --skip-stt Skip speech-to-text for videos
73
87
  --dry-run Show what would be processed without running AI
74
88
  --show-prompts Print the prompts file and exit
75
- --show-clis Print the default ITT/VTT/STT CLI templates
76
- --help Show this help
77
89
  --<prompt-name> "text" Override any prompt template from vision_prompts.md
78
90
  `);
79
91
  }
80
92
 
81
- export function parseArgs(argv) {
82
- const args = { command: "", file: "", output: "", forceNew: false, verbose: false, label: false, edit: false, noBrowser: false, chat: false, port: 3001, compile: false, cli: false, scriptOutputDir: "", mediaOutputDir: "", variant: [] };
83
- let i = 2;
84
- if (argv[i]) args.command = argv[i++];
85
- if (argv[i] && !argv[i].startsWith("--")) args.file = argv[i++];
86
- while (i < argv.length) {
87
- const flag = argv[i++];
88
- if (flag === "--output" && argv[i]) args.output = argv[i++];
89
- else if (flag === "--script-output-dir" && argv[i]) args.scriptOutputDir = argv[i++];
90
- else if (flag === "--media-output-dir" && argv[i]) args.mediaOutputDir = argv[i++];
91
- else if (flag === "--cli") args.cli = true;
92
- else if (flag === "--compile") args.compile = true;
93
- else if (flag === "--force-new") args.forceNew = true;
94
- else if (flag === "--verbose") args.verbose = true;
95
- else if (flag === "--label") args.label = true;
96
- else if (flag === "--edit") args.edit = true;
97
- else if (flag === "--no-browser") args.noBrowser = true;
98
- else if (flag === "--port" && argv[i]) args.port = parseInt(argv[i], 10);
99
- else if (flag.startsWith("--port=")) args.port = parseInt(flag.split("=")[1], 10);
100
- else if (flag === "--variant" && argv[i]) args.variant.push(argv[i++]);
101
- else if (flag.startsWith("--variant=")) args.variant.push(flag.split("=")[1]);
102
- }
103
- return args;
104
- }
105
93
 
106
94
  /**
107
95
  * Render one aspect ratio with compact progress output.
@@ -222,13 +210,32 @@ function renderOne(streamTree, aspect, outputPath, verbose) {
222
210
  }
223
211
 
224
212
  async function main() {
225
- const args = parseArgs(process.argv);
226
-
227
213
  if (!args.command || args.command === "help" || args.command === "--help") {
228
214
  usage();
229
215
  process.exit(0);
230
216
  }
231
217
 
218
+ // --show-clis works as a command or a flag
219
+ if (args.showClis || args.command === "--show-clis") {
220
+ console.log(`Default CLI
221
+ tti=${DEFAULT_TTI_CLI}
222
+
223
+ ttv=${DEFAULT_TTV_CLI || "(empty — falls back to TTI + ffmpeg to produce a 3s MP4)"}
224
+
225
+ tts=${DEFAULT_TTS_CLI}
226
+
227
+ stt=${DEFAULT_STT_CLI}
228
+
229
+ itt=${DEFAULT_ITT_CLI}
230
+
231
+ vtt=${DEFAULT_VTT_CLI || "(empty — uses ITT via frame extraction)"}
232
+
233
+ agent=${DEFAULT_AGENT_CLI}
234
+
235
+ edit=${DEFAULT_EDIT_CLI}`);
236
+ process.exit(0);
237
+ }
238
+
232
239
  if (args.command === "vision") {
233
240
  const { main: visionMain } = await import("../vision/cli.mjs");
234
241
  await visionMain(process.argv);
@@ -487,40 +494,6 @@ function hasScript(root) {
487
494
  }
488
495
  walkMedia(descriptive.children);
489
496
 
490
- // ── Check 2: CLI tool availability (--cli flag) ────────────────────
491
- if (args.cli) {
492
- function extractCmd(configStr) {
493
- if (!configStr) return null;
494
- return configStr.trim().split(/\s+/)[0] || null;
495
- }
496
-
497
- const pipelineConfigs = [
498
- { type: "TTS", config: descriptive.tts, defaultCmd: "edge-tts", needsCheck: hasScript(descriptive) },
499
- { type: "STT", config: descriptive.stt, defaultCmd: "whisper", needsCheck: hasScript(descriptive) },
500
- { type: "TTI", config: descriptive.tti, defaultCmd: "pi", needsCheck: needsTti },
501
- { type: "TTV", config: descriptive.ttv, defaultCmd: "pi", needsCheck: needsTtv },
502
- ];
503
-
504
- const checked = new Set();
505
- for (const p of pipelineConfigs) {
506
- if (!p.needsCheck) continue;
507
- const cmd = extractCmd(p.config) || p.defaultCmd;
508
- if (checked.has(cmd)) continue;
509
- checked.add(cmd);
510
- const { execSync } = await import("node:child_process");
511
- try {
512
- execSync(`which ${cmd}`, { stdio: "ignore" });
513
- } catch {
514
- const hints = {
515
- "pi": " (pip install pi-sdk)",
516
- "edge-tts": " (pip install edge-tts)",
517
- "whisper": " (pip install openai-whisper)",
518
- };
519
- errors.push(`CLI tool "${cmd}" not found${hints[cmd] || ""}. Required by ${p.type}`);
520
- }
521
- }
522
- }
523
-
524
497
  // ── Results ─────────────────────────────────────────────────────────
525
498
  for (const w of warnings) emitWarn(w);
526
499
  if (errors.length > 0) {