@a-company/atelier 0.36.0 → 0.38.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 (99) hide show
  1. package/dist/{chunk-JPZ4F4PW.js → chunk-3ARBOSWY.js} +64 -5
  2. package/dist/chunk-3ARBOSWY.js.map +1 -0
  3. package/dist/cli.js +11469 -413
  4. package/dist/cli.js.map +1 -1
  5. package/dist/{dist-M67UZGFQ.js → dist-3YQK6PI6.js} +2 -2
  6. package/dist/index.cjs +3193 -227
  7. package/dist/index.cjs.map +1 -1
  8. package/dist/index.d.cts +701 -8
  9. package/dist/index.d.ts +701 -8
  10. package/dist/index.js +7237 -72
  11. package/dist/index.js.map +1 -1
  12. package/dist/mcp.js +2898 -507
  13. package/dist/mcp.js.map +1 -1
  14. package/package.json +14 -9
  15. package/src/web/inline-app.ts +55 -4
  16. package/src/web/timeline-state-types.ts +28 -0
  17. package/src/web/timeline-view.test.ts +99 -0
  18. package/src/web/timeline-view.ts +339 -0
  19. package/src/web/workspace-app.ts +3146 -0
  20. package/templates/workspace/.claude/agents/atelier-iris.md +75 -0
  21. package/templates/workspace/.claude/agents/atelier-lux.md +67 -0
  22. package/templates/workspace/.claude/agents/atelier-quill.md +61 -0
  23. package/templates/workspace/.gitignore +30 -0
  24. package/templates/workspace/.paradigm/personas/_shared/cascade-merge.md +172 -0
  25. package/templates/workspace/CLAUDE.md +93 -0
  26. package/templates/workspace/README.md +75 -0
  27. package/templates/workspace/SETUP.md +127 -0
  28. package/templates/workspace/_brand/.atelier-brand.yaml +34 -0
  29. package/templates/workspace/_brand/DESIGN.md +56 -0
  30. package/templates/workspace/_brand/SCRIPT.md +41 -0
  31. package/templates/workspace/_brand/STORYBOARD.md +33 -0
  32. package/templates/workspace/_packs/README.md +54 -0
  33. package/templates/workspace/projects/README.md +49 -0
  34. package/templates/workspace/workspace.atelier +22 -0
  35. package/university/content/notes/N-atel-001-first-render.md +114 -0
  36. package/university/content/notes/N-atel-001-install-and-launch.md +84 -0
  37. package/university/content/notes/N-atel-001-what-is-atelier.md +51 -0
  38. package/university/content/notes/N-atel-101-easings.md +97 -0
  39. package/university/content/notes/N-atel-101-layers.md +106 -0
  40. package/university/content/notes/N-atel-101-states-and-deltas.md +94 -0
  41. package/university/content/notes/N-atel-101-the-atelier-format.md +72 -0
  42. package/university/content/notes/N-atel-201-authoring-tools.md +141 -0
  43. package/university/content/notes/N-atel-201-mcp-overview.md +86 -0
  44. package/university/content/notes/N-atel-201-patterns.md +108 -0
  45. package/university/content/notes/N-atel-201-visual-and-effects.md +125 -0
  46. package/university/content/notes/N-atel-301-composition-and-overlays.md +141 -0
  47. package/university/content/notes/N-atel-301-effects.md +136 -0
  48. package/university/content/notes/N-atel-301-images-and-video.md +126 -0
  49. package/university/content/notes/N-atel-301-shapes-and-text.md +118 -0
  50. package/university/content/notes/N-atel-401-hierarchical-states.md +71 -0
  51. package/university/content/notes/N-atel-401-motion-deep-dive.md +106 -0
  52. package/university/content/notes/N-atel-401-presets-and-templates.md +98 -0
  53. package/university/content/notes/N-atel-401-transitions.md +94 -0
  54. package/university/content/notes/N-atel-501-detected-vs-user-edited.md +76 -0
  55. package/university/content/notes/N-atel-501-layer-tag-isolation.md +62 -0
  56. package/university/content/notes/N-atel-501-silence-trim.md +98 -0
  57. package/university/content/notes/N-atel-501-transcribe-and-captions.md +98 -0
  58. package/university/content/notes/N-atel-601-carousel.md +71 -0
  59. package/university/content/notes/N-atel-601-overlay-rules.md +96 -0
  60. package/university/content/notes/N-atel-601-recipe-tools-and-apply.md +84 -0
  61. package/university/content/notes/N-atel-601-studio-recipe.md +103 -0
  62. package/university/content/notes/N-atel-701-choosing-output.md +68 -0
  63. package/university/content/notes/N-atel-701-png-and-frames.md +84 -0
  64. package/university/content/notes/N-atel-701-vector.md +85 -0
  65. package/university/content/notes/N-atel-701-video.md +88 -0
  66. package/university/content/notes/N-atel-801-editing-surface.md +69 -0
  67. package/university/content/notes/N-atel-801-live-bridge.md +84 -0
  68. package/university/content/notes/N-atel-801-studio-app.md +72 -0
  69. package/university/content/notes/N-atel-801-symbiotic-loop.md +56 -0
  70. package/university/content/paths/LP-atel-001.yaml +21 -0
  71. package/university/content/paths/LP-atel-101.yaml +22 -0
  72. package/university/content/paths/LP-atel-201.yaml +23 -0
  73. package/university/content/paths/LP-atel-301.yaml +22 -0
  74. package/university/content/paths/LP-atel-401.yaml +22 -0
  75. package/university/content/paths/LP-atel-501.yaml +22 -0
  76. package/university/content/paths/LP-atel-601.yaml +22 -0
  77. package/university/content/paths/LP-atel-701.yaml +22 -0
  78. package/university/content/paths/LP-atel-801.yaml +22 -0
  79. package/university/content/quizzes/Q-atel-001-orientation.yaml +66 -0
  80. package/university/content/quizzes/Q-atel-101-document-model.yaml +66 -0
  81. package/university/content/quizzes/Q-atel-201-mcp-authoring.yaml +66 -0
  82. package/university/content/quizzes/Q-atel-301-visual-system.yaml +66 -0
  83. package/university/content/quizzes/Q-atel-401-state-machines.yaml +66 -0
  84. package/university/content/quizzes/Q-atel-501-video-pipeline.yaml +66 -0
  85. package/university/content/quizzes/Q-atel-601-recipes.yaml +66 -0
  86. package/university/content/quizzes/Q-atel-701-export.yaml +66 -0
  87. package/university/content/quizzes/Q-atel-801-studio-loop.yaml +66 -0
  88. package/university/index.yaml +720 -0
  89. package/university/pack.yaml +21 -0
  90. package/dist/chunk-5QQESXI6.js +0 -4432
  91. package/dist/chunk-5QQESXI6.js.map +0 -1
  92. package/dist/chunk-JPZ4F4PW.js.map +0 -1
  93. package/dist/cli.cjs +0 -6313
  94. package/dist/cli.cjs.map +0 -1
  95. package/dist/cli.d.cts +0 -1
  96. package/dist/cli.d.ts +0 -1
  97. package/dist/mcp.cjs +0 -5462
  98. package/dist/mcp.cjs.map +0 -1
  99. /package/dist/{dist-M67UZGFQ.js.map → dist-3YQK6PI6.js.map} +0 -0
package/dist/index.cjs CHANGED
@@ -409,12 +409,15 @@ function resolveFrame(doc, stateName, frame, overrideDeltas) {
409
409
  throw new Error(`State "${stateName}" not found in document "${doc.name}"`);
410
410
  }
411
411
  const deltasByLayerProperty = groupDeltas(overrideDeltas ?? state.deltas);
412
+ const ancestorClipStartFrame = computeAncestorClipStartFrames(doc);
412
413
  const resolvedLayers = doc.layers.map((layer) => {
413
414
  const computedProperties = {};
414
415
  const layerDeltas = deltasByLayerProperty.get(layer.id);
415
416
  if (layerDeltas) {
417
+ const offset = ancestorClipStartFrame.get(layer.id) ?? 0;
418
+ const localFrame = frame - offset;
416
419
  for (const [property, deltas] of layerDeltas) {
417
- const value = resolvePropertyAtFrame(deltas, frame);
420
+ const value = resolvePropertyAtFrame(deltas, localFrame);
418
421
  if (value !== void 0) {
419
422
  computedProperties[property] = value;
420
423
  }
@@ -435,6 +438,26 @@ function resolveFrame(doc, stateName, frame, overrideDeltas) {
435
438
  });
436
439
  return { frame, stateName, layers: resolvedLayers };
437
440
  }
441
+ function computeAncestorClipStartFrames(doc) {
442
+ const byId = /* @__PURE__ */ new Map();
443
+ for (const l of doc.layers) byId.set(l.id, l);
444
+ const result = /* @__PURE__ */ new Map();
445
+ for (const layer of doc.layers) {
446
+ let cursor = byId.get(layer.parentId ?? "");
447
+ const visited = /* @__PURE__ */ new Set();
448
+ while (cursor) {
449
+ if (visited.has(cursor.id)) break;
450
+ visited.add(cursor.id);
451
+ if (cursor.visual.type === "video") {
452
+ const v = cursor.visual;
453
+ result.set(layer.id, v.startFrame ?? 0);
454
+ break;
455
+ }
456
+ cursor = byId.get(cursor.parentId ?? "");
457
+ }
458
+ }
459
+ return result;
460
+ }
438
461
  function groupDeltas(deltas) {
439
462
  const map = /* @__PURE__ */ new Map();
440
463
  for (const delta of deltas) {
@@ -1044,7 +1067,40 @@ function renderVideo(ctx, eff, sourceTime, provider) {
1044
1067
  if (!src) return;
1045
1068
  const frame = provider(src, sourceTime, eff.width, eff.height);
1046
1069
  if (!frame) return;
1047
- ctx.drawImage(frame, 0, 0, eff.width, eff.height);
1070
+ const fit = visual.objectFit ?? "contain";
1071
+ if (fit === "fill") {
1072
+ ctx.drawImage(frame, 0, 0, eff.width, eff.height);
1073
+ return;
1074
+ }
1075
+ const src_w = frame.videoWidth ?? frame.width ?? 0;
1076
+ const src_h = frame.videoHeight ?? frame.height ?? 0;
1077
+ if (src_w <= 0 || src_h <= 0) {
1078
+ ctx.drawImage(frame, 0, 0, eff.width, eff.height);
1079
+ return;
1080
+ }
1081
+ const srcAspect = src_w / src_h;
1082
+ const dstAspect = eff.width / eff.height;
1083
+ let dw, dh, dx, dy;
1084
+ if (fit === "cover") {
1085
+ if (srcAspect > dstAspect) {
1086
+ dh = eff.height;
1087
+ dw = dh * srcAspect;
1088
+ } else {
1089
+ dw = eff.width;
1090
+ dh = dw / srcAspect;
1091
+ }
1092
+ } else {
1093
+ if (srcAspect > dstAspect) {
1094
+ dw = eff.width;
1095
+ dh = dw / srcAspect;
1096
+ } else {
1097
+ dh = eff.height;
1098
+ dw = dh * srcAspect;
1099
+ }
1100
+ }
1101
+ dx = (eff.width - dw) / 2;
1102
+ dy = (eff.height - dh) / 2;
1103
+ ctx.drawImage(frame, dx, dy, dw, dh);
1048
1104
  }
1049
1105
  function renderRef(ctx, eff, opts, _parentDoc) {
1050
1106
  const visual = eff.visual;
@@ -1155,8 +1211,11 @@ function renderFrame(ctx, resolvedFrame, doc, optsOrCache) {
1155
1211
  maxRefDepth = opts.maxRefDepth ?? 4;
1156
1212
  }
1157
1213
  const { width, height } = doc.canvas;
1158
- ctx.fillStyle = doc.canvas.background ?? "transparent";
1159
- ctx.fillRect(0, 0, width, height);
1214
+ ctx.clearRect(0, 0, width, height);
1215
+ if (doc.canvas.background) {
1216
+ ctx.fillStyle = doc.canvas.background;
1217
+ ctx.fillRect(0, 0, width, height);
1218
+ }
1160
1219
  const effMap = /* @__PURE__ */ new Map();
1161
1220
  const effList = [];
1162
1221
  const videoSourceTimeMap = /* @__PURE__ */ new Map();
@@ -1421,9 +1480,12 @@ var init_dist3 = __esm({
1421
1480
  });
1422
1481
 
1423
1482
  // src/index.ts
1424
- var index_exports = {};
1425
- __export(index_exports, {
1483
+ var src_exports = {};
1484
+ __export(src_exports, {
1485
+ ARTIFACT_FILENAMES: () => ARTIFACT_FILENAMES,
1426
1486
  CanvasUnavailableError: () => CanvasUnavailableError,
1487
+ LEARNING_MODES: () => LEARNING_MODES,
1488
+ LEARNING_MODE_VERSION: () => LEARNING_MODE_VERSION,
1427
1489
  RECIPE_VERSION: () => RECIPE_VERSION,
1428
1490
  VIDEO_CUTLIST_VERSION: () => VIDEO_CUTLIST_VERSION,
1429
1491
  VIDEO_PROJECT_VERSION: () => VIDEO_PROJECT_VERSION,
@@ -1438,6 +1500,7 @@ __export(index_exports, {
1438
1500
  applyRecipeToTrimOptions: () => applyRecipeToTrimOptions,
1439
1501
  applySplit: () => applySplit,
1440
1502
  applyTextEdit: () => applyTextEdit,
1503
+ artifactsCommand: () => artifactsCommand,
1441
1504
  assetsCommand: () => assetsCommand,
1442
1505
  buildCaptionLayers: () => buildCaptionLayers,
1443
1506
  buildFfmpegArgs: () => buildFfmpegArgs,
@@ -1445,8 +1508,15 @@ __export(index_exports, {
1445
1508
  carouselCommand: () => carouselCommand,
1446
1509
  carouselFileName: () => carouselFileName,
1447
1510
  checkFfmpeg: () => checkFfmpeg,
1511
+ classifyMediaFile: () => classifyMediaFile,
1448
1512
  composeCarouselFrameDoc: () => composeCarouselFrameDoc,
1513
+ composeCarouselProject: () => composeCarouselProject,
1514
+ composeImageProject: () => composeImageProject,
1515
+ composeVideoProject: () => composeVideoProject,
1516
+ createDoc: () => createDoc,
1449
1517
  createVideoProject: () => createVideoProject,
1518
+ deleteDoc: () => deleteDoc,
1519
+ duplicateDoc: () => duplicateDoc,
1450
1520
  effectiveSpan: () => effectiveSpan,
1451
1521
  expandInputs: () => expandInputs,
1452
1522
  exportImageCommand: () => exportImageCommand,
@@ -1459,26 +1529,38 @@ __export(index_exports, {
1459
1529
  getVariables: () => getVariables,
1460
1530
  groupIntoPhrases: () => groupIntoPhrases,
1461
1531
  infoCommand: () => infoCommand,
1532
+ initCommand: () => initCommand,
1533
+ isInsideGitRepo: () => isInsideGitRepo,
1534
+ isValidDocName: () => isValidDocName,
1535
+ learningModePath: () => learningModePath,
1536
+ listDocs: () => listDocs,
1537
+ loadArtifactsFromProject: () => loadArtifactsFromProject,
1462
1538
  loadCanvasModule: () => loadCanvasModule,
1463
1539
  loadRecipe: () => loadRecipe,
1464
1540
  loadVideoProject: () => loadVideoProject,
1465
1541
  mergeTranscriptWithExisting: () => mergeTranscriptWithExisting,
1542
+ parseModeAnswer: () => parseModeAnswer,
1466
1543
  parseWhisperCppJson: () => parseWhisperCppJson,
1467
1544
  probeWhisper: () => probeWhisper,
1468
1545
  readComposition: () => readComposition,
1469
1546
  readCutList: () => readCutList,
1547
+ readLearningMode: () => readLearningMode,
1470
1548
  readTranscript: () => readTranscript,
1471
1549
  recipeCommand: () => recipeCommand,
1472
1550
  recipeToYaml: () => recipeToYaml,
1473
1551
  renderCommand: () => renderCommand,
1474
1552
  renderDocument: () => renderDocument,
1475
1553
  renderDocumentToPng: () => renderDocumentToPng,
1554
+ renderLearningModeYaml: () => renderLearningModeYaml,
1476
1555
  renderRecipeWithDefaults: () => renderRecipeWithDefaults,
1477
1556
  resolveExportDimensions: () => resolveExportDimensions,
1478
1557
  resolveRecipePath: () => resolveRecipePath,
1479
1558
  resolveStill: () => resolveStill,
1480
1559
  rewriteCaptionLayers: () => rewriteCaptionLayers,
1481
1560
  rewriteCutLayers: () => rewriteCutLayers,
1561
+ routeIngest: () => routeIngest,
1562
+ routeIngestWithTarget: () => routeIngestWithTarget,
1563
+ runInit: () => runInit,
1482
1564
  runWhisperCpp: () => runWhisperCpp,
1483
1565
  scaffoldRecipeYaml: () => scaffoldRecipeYaml,
1484
1566
  stillCommand: () => stillCommand,
@@ -1492,9 +1574,10 @@ __export(index_exports, {
1492
1574
  variablesCommand: () => variablesCommand,
1493
1575
  writeComposition: () => writeComposition,
1494
1576
  writeCutList: () => writeCutList,
1577
+ writeLearningMode: () => writeLearningMode,
1495
1578
  writeTranscript: () => writeTranscript
1496
1579
  });
1497
- module.exports = __toCommonJS(index_exports);
1580
+ module.exports = __toCommonJS(src_exports);
1498
1581
 
1499
1582
  // src/commands/validate.ts
1500
1583
  var import_node_fs = require("fs");
@@ -1516,7 +1599,18 @@ var import_zod12 = require("zod");
1516
1599
  var import_zod13 = require("zod");
1517
1600
  var import_zod14 = require("zod");
1518
1601
  var import_zod15 = require("zod");
1602
+ var import_zod16 = require("zod");
1603
+ var import_zod17 = require("zod");
1604
+ var import_zod18 = require("zod");
1519
1605
  var import_yaml = require("yaml");
1606
+ var import_zod19 = require("zod");
1607
+ var import_zod20 = require("zod");
1608
+ var import_zod21 = require("zod");
1609
+ var import_zod22 = require("zod");
1610
+ var import_zod23 = require("zod");
1611
+ var import_yaml2 = require("yaml");
1612
+ var import_zod24 = require("zod");
1613
+ var import_zod25 = require("zod");
1520
1614
  var PixelSchema = import_zod.z.number();
1521
1615
  var PercentageSchema = import_zod.z.string().regex(/^-?\d+(\.\d+)?%$/, {
1522
1616
  message: 'Percentage must be a number followed by %, e.g. "50%"'
@@ -1769,6 +1863,19 @@ var VideoVisualSchema = import_zod7.z.object({
1769
1863
  muted: import_zod7.z.boolean().optional(),
1770
1864
  objectFit: import_zod7.z.enum(["contain", "cover", "fill"]).optional()
1771
1865
  });
1866
+ var AudioVisualSchema = import_zod7.z.object({
1867
+ type: import_zod7.z.literal("audio"),
1868
+ assetId: import_zod7.z.string().min(1, "assetId is required"),
1869
+ src: import_zod7.z.string().optional(),
1870
+ startFrame: import_zod7.z.number().int().min(0).optional(),
1871
+ sourceOffset: import_zod7.z.number().min(0).optional(),
1872
+ sourceEnd: import_zod7.z.number().positive().optional(),
1873
+ playbackRate: import_zod7.z.number().positive().optional(),
1874
+ volume: import_zod7.z.number().min(0).max(1).optional(),
1875
+ muted: import_zod7.z.boolean().optional(),
1876
+ fadeIn: import_zod7.z.number().min(0).optional(),
1877
+ fadeOut: import_zod7.z.number().min(0).optional()
1878
+ });
1772
1879
  var GroupVisualSchema = import_zod7.z.object({
1773
1880
  type: import_zod7.z.literal("group")
1774
1881
  });
@@ -1783,6 +1890,7 @@ var VisualSchema = import_zod7.z.discriminatedUnion("type", [
1783
1890
  TextVisualSchema,
1784
1891
  ImageVisualSchema,
1785
1892
  VideoVisualSchema,
1893
+ AudioVisualSchema,
1786
1894
  GroupVisualSchema,
1787
1895
  RefVisualSchema
1788
1896
  ]);
@@ -1864,13 +1972,6 @@ var DeltaSchema = import_zod9.z.object({
1864
1972
  description: import_zod9.z.string().optional(),
1865
1973
  tags: import_zod9.z.array(import_zod9.z.string()).optional()
1866
1974
  });
1867
- var AudioSchema = import_zod10.z.object({
1868
- src: import_zod10.z.string().min(1, "Audio src is required"),
1869
- offset: import_zod10.z.number().min(0, "Audio offset must be non-negative").optional(),
1870
- volume: import_zod10.z.number().min(0).max(1, "Audio volume must be 0\u20131").optional(),
1871
- loop: import_zod10.z.boolean().optional(),
1872
- startFrame: import_zod10.z.number().int().min(0, "Audio startFrame must be a non-negative integer").optional()
1873
- });
1874
1975
  var StateTransitionConfigSchema = import_zod10.z.object({
1875
1976
  duration: import_zod10.z.number().int().positive("Transition duration must be a positive integer (frames)"),
1876
1977
  easing: EasingSchema.optional()
@@ -1881,7 +1982,6 @@ var StateSchema = import_zod10.z.object({
1881
1982
  parent: import_zod10.z.string().optional(),
1882
1983
  duration: import_zod10.z.number().int().positive("State duration must be a positive integer (frames)"),
1883
1984
  deltas: import_zod10.z.array(DeltaSchema),
1884
- audio: AudioSchema.optional(),
1885
1985
  transitions: import_zod10.z.record(import_zod10.z.string(), StateTransitionConfigSchema).optional()
1886
1986
  });
1887
1987
  var PresetDeltaSchema = import_zod11.z.object({
@@ -1921,61 +2021,119 @@ var AssetSchema = import_zod13.z.object({
1921
2021
  height: import_zod13.z.number().int().positive()
1922
2022
  }).optional()
1923
2023
  });
2024
+ var TimelineClipSchema = import_zod15.z.object({
2025
+ layerId: import_zod15.z.string().min(1, "TimelineClip.layerId is required"),
2026
+ startFrame: import_zod15.z.number().int().min(0, "startFrame must be a non-negative integer"),
2027
+ endFrame: import_zod15.z.number().int().min(0, "endFrame must be a non-negative integer"),
2028
+ source: import_zod15.z.string().min(1, "TimelineClip.source is required")
2029
+ });
2030
+ var TimelineTrackSchema = import_zod15.z.object({
2031
+ id: import_zod15.z.string().min(1, "TimelineTrack.id is required"),
2032
+ kind: import_zod15.z.enum(["video", "audio"]),
2033
+ clips: import_zod15.z.array(TimelineClipSchema)
2034
+ });
2035
+ var TimelineSchema = import_zod15.z.object({
2036
+ fps: import_zod15.z.number().int().positive("Timeline.fps must be a positive integer"),
2037
+ totalFrames: import_zod15.z.number().int().min(0, "Timeline.totalFrames must be a non-negative integer"),
2038
+ tracks: import_zod15.z.array(TimelineTrackSchema)
2039
+ });
2040
+ var SlideTransitionSchema = import_zod16.z.object({
2041
+ kind: import_zod16.z.enum(["cut", "crossfade", "fade"]),
2042
+ durationFrames: import_zod16.z.number().int().min(0, "transition durationFrames must be a non-negative integer").optional(),
2043
+ color: import_zod16.z.string().optional()
2044
+ });
2045
+ var SlideRefSchema = import_zod16.z.object({
2046
+ src: import_zod16.z.string().min(1, "SlideRef.src is required"),
2047
+ duration: import_zod16.z.number().int().min(0, "SlideRef.duration must be a non-negative integer (frames)").optional(),
2048
+ transition: SlideTransitionSchema.optional(),
2049
+ label: import_zod16.z.string().optional()
2050
+ });
2051
+ var DesignRelationSchema = import_zod17.z.enum(["derived-from", "sibling", "references"]);
2052
+ var AtelierDesignRelationSchema = import_zod17.z.object({
2053
+ source: import_zod17.z.string().min(1, "AtelierDesignRelation.source is required"),
2054
+ relation: DesignRelationSchema,
2055
+ note: import_zod17.z.string().optional()
2056
+ });
2057
+ var DesignTypographySchema = import_zod17.z.object({
2058
+ headingFamily: import_zod17.z.string().optional(),
2059
+ bodyFamily: import_zod17.z.string().optional(),
2060
+ monoFamily: import_zod17.z.string().optional(),
2061
+ sizes: import_zod17.z.record(import_zod17.z.string(), import_zod17.z.number().positive()).optional(),
2062
+ weights: import_zod17.z.record(import_zod17.z.string(), import_zod17.z.string()).optional()
2063
+ });
2064
+ var DesignMotionSchema = import_zod17.z.object({
2065
+ defaultEasing: import_zod17.z.string().optional(),
2066
+ defaultDuration: import_zod17.z.number().int().positive().optional()
2067
+ });
2068
+ var AtelierDesignSchema = import_zod17.z.object({
2069
+ palette: import_zod17.z.record(import_zod17.z.string(), import_zod17.z.string()).optional(),
2070
+ typography: DesignTypographySchema.optional(),
2071
+ motion: DesignMotionSchema.optional(),
2072
+ relatedTo: import_zod17.z.array(AtelierDesignRelationSchema).optional()
2073
+ });
1924
2074
  var CanvasSchema = import_zod14.z.object({
1925
2075
  width: import_zod14.z.number().int().positive("Canvas width must be a positive integer"),
1926
2076
  height: import_zod14.z.number().int().positive("Canvas height must be a positive integer"),
1927
2077
  fps: import_zod14.z.number().int().positive("FPS must be a positive integer"),
1928
2078
  background: import_zod14.z.string().optional()
1929
2079
  });
2080
+ var ProjectKindSchema = import_zod14.z.enum(["video", "image", "carousel"]);
1930
2081
  var AtelierDocumentSchema = import_zod14.z.object({
1931
2082
  version: import_zod14.z.string().min(1, "Version is required"),
1932
2083
  name: import_zod14.z.string().min(1, "Animation name is required"),
2084
+ kind: ProjectKindSchema.optional(),
1933
2085
  description: import_zod14.z.string().optional(),
1934
2086
  tags: import_zod14.z.array(import_zod14.z.string()).optional(),
1935
2087
  canvas: CanvasSchema,
2088
+ /** Per-file design / brand block (self-contained — no auto-merge). */
2089
+ design: AtelierDesignSchema.optional(),
1936
2090
  variables: import_zod14.z.record(import_zod14.z.string(), VariableSchema).optional(),
1937
2091
  assets: import_zod14.z.record(import_zod14.z.string(), AssetSchema).optional(),
1938
2092
  presets: import_zod14.z.record(import_zod14.z.string(), PresetSchema).optional(),
1939
2093
  layers: import_zod14.z.array(LayerSchema),
1940
- states: import_zod14.z.record(import_zod14.z.string(), StateSchema)
2094
+ states: import_zod14.z.record(import_zod14.z.string(), StateSchema),
2095
+ /** Derived timeline summary for video-kind projects. Optional. */
2096
+ timeline: TimelineSchema.optional(),
2097
+ /** Carousel slides — only meaningful when kind === "carousel". */
2098
+ slides: import_zod14.z.array(SlideRefSchema).optional()
1941
2099
  });
1942
- var SilencePolicySchema = import_zod15.z.object({
1943
- noise: import_zod15.z.string().optional(),
1944
- min_silence: import_zod15.z.number().nonnegative().optional(),
1945
- default_padding_pre: import_zod15.z.number().nonnegative().optional(),
1946
- default_padding_post: import_zod15.z.number().nonnegative().optional(),
1947
- match_tolerance: import_zod15.z.number().nonnegative().optional()
2100
+ var SilencePolicySchema = import_zod18.z.object({
2101
+ noise: import_zod18.z.string().optional(),
2102
+ min_silence: import_zod18.z.number().nonnegative().optional(),
2103
+ default_padding_pre: import_zod18.z.number().nonnegative().optional(),
2104
+ default_padding_post: import_zod18.z.number().nonnegative().optional(),
2105
+ match_tolerance: import_zod18.z.number().nonnegative().optional()
1948
2106
  }).strict();
1949
- var CaptionStyleSchema = import_zod15.z.object({
1950
- font_family: import_zod15.z.string().optional(),
1951
- font_size: import_zod15.z.number().positive().optional(),
1952
- font_weight: import_zod15.z.union([import_zod15.z.literal("normal"), import_zod15.z.literal("bold"), import_zod15.z.number()]).optional(),
1953
- text_align: import_zod15.z.enum(["left", "center", "right"]).optional(),
1954
- color: import_zod15.z.string().optional(),
1955
- y_ratio: import_zod15.z.number().min(0).max(1).optional(),
1956
- width_ratio: import_zod15.z.number().min(0).max(1).optional(),
1957
- fade_seconds: import_zod15.z.number().nonnegative().optional()
2107
+ var CaptionStyleSchema = import_zod18.z.object({
2108
+ font_family: import_zod18.z.string().optional(),
2109
+ font_size: import_zod18.z.number().positive().optional(),
2110
+ font_weight: import_zod18.z.union([import_zod18.z.literal("normal"), import_zod18.z.literal("bold"), import_zod18.z.number()]).optional(),
2111
+ text_align: import_zod18.z.enum(["left", "center", "right"]).optional(),
2112
+ color: import_zod18.z.string().optional(),
2113
+ y_ratio: import_zod18.z.number().min(0).max(1).optional(),
2114
+ width_ratio: import_zod18.z.number().min(0).max(1).optional(),
2115
+ fade_seconds: import_zod18.z.number().nonnegative().optional()
1958
2116
  }).strict();
1959
- var CaptionGroupingSchema = import_zod15.z.object({
1960
- max_words: import_zod15.z.number().int().positive().optional(),
1961
- pause_gap: import_zod15.z.number().nonnegative().optional()
2117
+ var CaptionGroupingSchema = import_zod18.z.object({
2118
+ max_words: import_zod18.z.number().int().positive().optional(),
2119
+ pause_gap: import_zod18.z.number().nonnegative().optional()
1962
2120
  }).strict();
1963
- var OverlayAnchorSchema = import_zod15.z.enum([
2121
+ var OverlayAnchorSchema = import_zod18.z.enum([
1964
2122
  "top-left",
1965
2123
  "top-right",
1966
2124
  "bottom-left",
1967
2125
  "bottom-right"
1968
2126
  ]);
1969
- var OverlayTextStyleSchema = import_zod15.z.object({
1970
- font_family: import_zod15.z.string().optional(),
1971
- font_size: import_zod15.z.number().positive().optional(),
1972
- font_weight: import_zod15.z.union([import_zod15.z.literal("normal"), import_zod15.z.literal("bold"), import_zod15.z.number()]).optional(),
1973
- color: import_zod15.z.string().optional()
2127
+ var OverlayTextStyleSchema = import_zod18.z.object({
2128
+ font_family: import_zod18.z.string().optional(),
2129
+ font_size: import_zod18.z.number().positive().optional(),
2130
+ font_weight: import_zod18.z.union([import_zod18.z.literal("normal"), import_zod18.z.literal("bold"), import_zod18.z.number()]).optional(),
2131
+ color: import_zod18.z.string().optional()
1974
2132
  }).strict();
1975
2133
  function validatePageNumberFormat(format, ctx) {
1976
2134
  if (format.length === 0) {
1977
2135
  ctx.addIssue({
1978
- code: import_zod15.z.ZodIssueCode.custom,
2136
+ code: import_zod18.z.ZodIssueCode.custom,
1979
2137
  message: "format must be a non-empty string"
1980
2138
  });
1981
2139
  return;
@@ -1984,7 +2142,7 @@ function validatePageNumberFormat(format, ctx) {
1984
2142
  const close = (format.match(/\}/g) ?? []).length;
1985
2143
  if (open !== close) {
1986
2144
  ctx.addIssue({
1987
- code: import_zod15.z.ZodIssueCode.custom,
2145
+ code: import_zod18.z.ZodIssueCode.custom,
1988
2146
  message: `format has unbalanced braces (${open} '{' vs ${close} '}')`
1989
2147
  });
1990
2148
  return;
@@ -1998,7 +2156,7 @@ function validatePageNumberFormat(format, ctx) {
1998
2156
  const inner = m[1];
1999
2157
  if (!groupRule.test(inner)) {
2000
2158
  ctx.addIssue({
2001
- code: import_zod15.z.ZodIssueCode.custom,
2159
+ code: import_zod18.z.ZodIssueCode.custom,
2002
2160
  message: `format placeholder "{${inner}}" is not recognized \u2014 expected {current}, {total}, {current:0Nd}, or {total:0Nd}`
2003
2161
  });
2004
2162
  return;
@@ -2008,44 +2166,44 @@ function validatePageNumberFormat(format, ctx) {
2008
2166
  }
2009
2167
  if (!sawCurrent && !sawTotal) {
2010
2168
  ctx.addIssue({
2011
- code: import_zod15.z.ZodIssueCode.custom,
2169
+ code: import_zod18.z.ZodIssueCode.custom,
2012
2170
  message: "format must contain at least one {current} or {total} placeholder"
2013
2171
  });
2014
2172
  }
2015
2173
  }
2016
- var OverlayHandleRuleSchema = import_zod15.z.object({
2017
- text: import_zod15.z.string().min(1),
2174
+ var OverlayHandleRuleSchema = import_zod18.z.object({
2175
+ text: import_zod18.z.string().min(1),
2018
2176
  anchor: OverlayAnchorSchema,
2019
- margin: import_zod15.z.number().nonnegative().optional(),
2177
+ margin: import_zod18.z.number().nonnegative().optional(),
2020
2178
  style: OverlayTextStyleSchema.optional()
2021
2179
  }).strict();
2022
- var OverlayPageNumberRuleSchema = import_zod15.z.object({
2023
- format: import_zod15.z.string().superRefine(validatePageNumberFormat),
2180
+ var OverlayPageNumberRuleSchema = import_zod18.z.object({
2181
+ format: import_zod18.z.string().superRefine(validatePageNumberFormat),
2024
2182
  anchor: OverlayAnchorSchema,
2025
- margin: import_zod15.z.number().nonnegative().optional(),
2183
+ margin: import_zod18.z.number().nonnegative().optional(),
2026
2184
  style: OverlayTextStyleSchema.optional()
2027
2185
  }).strict();
2028
- var OverlayRulesSchema = import_zod15.z.object({
2186
+ var OverlayRulesSchema = import_zod18.z.object({
2029
2187
  handle: OverlayHandleRuleSchema.optional(),
2030
2188
  page_number: OverlayPageNumberRuleSchema.optional()
2031
2189
  }).strict();
2032
- var StudioRecipeSchema = import_zod15.z.object({
2033
- version: import_zod15.z.string(),
2034
- name: import_zod15.z.string(),
2035
- description: import_zod15.z.string().optional(),
2036
- author: import_zod15.z.string().optional(),
2037
- tags: import_zod15.z.array(import_zod15.z.string()).optional(),
2190
+ var StudioRecipeSchema = import_zod18.z.object({
2191
+ version: import_zod18.z.string(),
2192
+ name: import_zod18.z.string(),
2193
+ description: import_zod18.z.string().optional(),
2194
+ author: import_zod18.z.string().optional(),
2195
+ tags: import_zod18.z.array(import_zod18.z.string()).optional(),
2038
2196
  silence_policy: SilencePolicySchema.optional(),
2039
2197
  caption_style: CaptionStyleSchema.optional(),
2040
2198
  caption_grouping: CaptionGroupingSchema.optional(),
2041
2199
  // Phase 1.5 — first-class overlay rules
2042
2200
  overlay_rules: OverlayRulesSchema.optional(),
2043
2201
  // Reserved — Phase 3 (parse-opaque)
2044
- caption_highlight: import_zod15.z.unknown().optional(),
2045
- transition_kit: import_zod15.z.unknown().optional(),
2046
- palette: import_zod15.z.unknown().optional(),
2047
- audio_policy: import_zod15.z.unknown().optional(),
2048
- aspect_targets: import_zod15.z.array(import_zod15.z.unknown()).optional()
2202
+ caption_highlight: import_zod18.z.unknown().optional(),
2203
+ transition_kit: import_zod18.z.unknown().optional(),
2204
+ palette: import_zod18.z.unknown().optional(),
2205
+ audio_policy: import_zod18.z.unknown().optional(),
2206
+ aspect_targets: import_zod18.z.array(import_zod18.z.unknown()).optional()
2049
2207
  }).strict();
2050
2208
  var RESERVED_RECIPE_FIELDS = [
2051
2209
  "caption_highlight",
@@ -2095,6 +2253,570 @@ function parseAtelier(yamlString) {
2095
2253
  }
2096
2254
  return validateDocument(parsed);
2097
2255
  }
2256
+ var SlotKindSchema = import_zod19.z.enum([
2257
+ "image",
2258
+ "video",
2259
+ "audio",
2260
+ "text",
2261
+ "font",
2262
+ "voice",
2263
+ "logo"
2264
+ ]);
2265
+ var SLOT_KINDS = SlotKindSchema.options;
2266
+ function parseSlotRef(ref) {
2267
+ if (typeof ref !== "string" || ref.length === 0) return null;
2268
+ const segments = ref.split(".");
2269
+ if (segments.length < 2) return null;
2270
+ if (segments.some((s) => s.length === 0)) return null;
2271
+ const [rawKind, category, ...rest] = segments;
2272
+ const kindParsed = SlotKindSchema.safeParse(rawKind);
2273
+ if (!kindParsed.success) return null;
2274
+ const specifier = rest.join(".");
2275
+ return { kind: kindParsed.data, category, specifier };
2276
+ }
2277
+ var SlotRefSchema = import_zod19.z.string().superRefine((value, ctx) => {
2278
+ const parsed = parseSlotRef(value);
2279
+ if (parsed !== null) return;
2280
+ const segments = value.split(".");
2281
+ if (segments.length < 2) {
2282
+ ctx.addIssue({
2283
+ code: import_zod19.z.ZodIssueCode.custom,
2284
+ message: `slot reference "${value}" must have at least two segments (<kind>.<open-tail>)`
2285
+ });
2286
+ return;
2287
+ }
2288
+ if (segments.some((s) => s.length === 0)) {
2289
+ ctx.addIssue({
2290
+ code: import_zod19.z.ZodIssueCode.custom,
2291
+ message: `slot reference "${value}" has an empty segment`
2292
+ });
2293
+ return;
2294
+ }
2295
+ const kindOk = SlotKindSchema.safeParse(segments[0]).success;
2296
+ if (!kindOk) {
2297
+ ctx.addIssue({
2298
+ code: import_zod19.z.ZodIssueCode.custom,
2299
+ message: `slot reference "${value}" has unknown kind "${segments[0]}" \u2014 expected one of ${SLOT_KINDS.join(", ")}`
2300
+ });
2301
+ return;
2302
+ }
2303
+ ctx.addIssue({
2304
+ code: import_zod19.z.ZodIssueCode.custom,
2305
+ message: `slot reference "${value}" is malformed`
2306
+ });
2307
+ });
2308
+ var TechniqueLibrarySchema = import_zod20.z.object({
2309
+ version: import_zod20.z.number().int().positive(),
2310
+ techniques: import_zod20.z.array(import_zod20.z.string().min(1))
2311
+ }).strict();
2312
+ var TECHNIQUE_LIBRARY_V1 = {
2313
+ version: 1,
2314
+ techniques: [
2315
+ "text.split-reveal",
2316
+ "image.ken-burns.slow",
2317
+ "super.weight-pulse-on-keyword",
2318
+ "background.color-shift-to-token"
2319
+ ]
2320
+ };
2321
+ var TECHNIQUE_LIBRARY_V2 = {
2322
+ version: 2,
2323
+ techniques: [
2324
+ // v1 (preserved)
2325
+ "text.split-reveal",
2326
+ "image.ken-burns.slow",
2327
+ "super.weight-pulse-on-keyword",
2328
+ "background.color-shift-to-token",
2329
+ // v2 additions — text
2330
+ "text.kinetic-keyword-emphasis",
2331
+ "text.typewriter",
2332
+ // v2 additions — image
2333
+ "image.zoom-in-during-beat",
2334
+ "image.cross-dissolve",
2335
+ "image.parallax-pan-slow",
2336
+ // v2 additions — transition
2337
+ "transition.whip-pan",
2338
+ "transition.match-cut",
2339
+ "transition.morph-cut",
2340
+ "transition.fade-through-color",
2341
+ // v2 additions — super (overlay text)
2342
+ "super.scale-in-spring",
2343
+ "super.drop-in-staggered",
2344
+ // v2 additions — audio
2345
+ "audio.fade-under-vo"
2346
+ ]
2347
+ };
2348
+ var TECHNIQUE_LIBRARIES = {
2349
+ 1: TECHNIQUE_LIBRARY_V1,
2350
+ 2: TECHNIQUE_LIBRARY_V2
2351
+ };
2352
+ function getTechniqueLibrary(version) {
2353
+ return TECHNIQUE_LIBRARIES[version] ?? null;
2354
+ }
2355
+ var DesignAudienceSchema = import_zod21.z.object({
2356
+ primary: import_zod21.z.string().min(1),
2357
+ secondary: import_zod21.z.string().optional(),
2358
+ platform: import_zod21.z.array(import_zod21.z.string().min(1)).min(1)
2359
+ }).strict();
2360
+ var DesignVoiceSchema = import_zod21.z.object({
2361
+ descriptors: import_zod21.z.array(import_zod21.z.string().min(1)).min(3).max(5),
2362
+ references: import_zod21.z.array(import_zod21.z.string().min(1)).optional()
2363
+ }).strict();
2364
+ var PaletteRoleSchema = import_zod21.z.enum([
2365
+ "background",
2366
+ "surface",
2367
+ "primary",
2368
+ "accent",
2369
+ "text-on-dark",
2370
+ "text-on-light"
2371
+ ]);
2372
+ var PaletteEntrySchema = import_zod21.z.object({
2373
+ token: import_zod21.z.string().min(1),
2374
+ hex: import_zod21.z.string().regex(/^#([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6}|[0-9A-Fa-f]{8})$/, {
2375
+ message: "hex must be a #RGB / #RRGGBB / #RRGGBBAA string"
2376
+ }),
2377
+ role: PaletteRoleSchema
2378
+ }).strict();
2379
+ var TypographyUsageSchema = import_zod21.z.enum(["display", "body", "caption", "mono"]);
2380
+ var TypographyEntrySchema = import_zod21.z.object({
2381
+ token: import_zod21.z.string().min(1),
2382
+ family: import_zod21.z.string().min(1),
2383
+ weights: import_zod21.z.array(import_zod21.z.number().int().min(1).max(1e3)).min(1),
2384
+ usage: TypographyUsageSchema
2385
+ }).strict();
2386
+ var MotionRegisterSchema = import_zod21.z.object({
2387
+ tempo: import_zod21.z.enum(["snappy", "steady", "calm", "languid"]),
2388
+ easing_bias: import_zod21.z.enum(["linear", "ease-out", "spring"]),
2389
+ camera_bias: import_zod21.z.enum(["static", "drift", "snap"]).optional()
2390
+ }).strict();
2391
+ var VisualRegisterSchema = import_zod21.z.object({
2392
+ palette: import_zod21.z.array(PaletteEntrySchema).min(1),
2393
+ typography: import_zod21.z.array(TypographyEntrySchema).min(1),
2394
+ motion_register: MotionRegisterSchema
2395
+ }).strict();
2396
+ var AspectRatioSchema = import_zod21.z.enum(["9:16", "1:1", "16:9", "4:5"]);
2397
+ var DesignConstraintsSchema = import_zod21.z.object({
2398
+ max_duration_seconds: import_zod21.z.number().positive().optional(),
2399
+ aspect_ratios: import_zod21.z.array(AspectRatioSchema).min(1),
2400
+ do_not: import_zod21.z.array(import_zod21.z.string().min(1))
2401
+ }).strict();
2402
+ var BrandLogoSchema = import_zod21.z.object({
2403
+ token: import_zod21.z.string().min(1),
2404
+ role: import_zod21.z.enum(["primary", "mark", "wordmark"])
2405
+ }).strict();
2406
+ var BrandHandleSchema = import_zod21.z.object({
2407
+ platform: import_zod21.z.string().min(1),
2408
+ value: import_zod21.z.string().min(1)
2409
+ }).strict();
2410
+ var BrandReferencesSchema = import_zod21.z.object({
2411
+ logos: import_zod21.z.array(BrandLogoSchema).min(1),
2412
+ handles: import_zod21.z.array(BrandHandleSchema).optional(),
2413
+ page_number_convention: import_zod21.z.string().optional()
2414
+ }).strict();
2415
+ var DesignVarianceSchema = import_zod21.z.object({
2416
+ field: import_zod21.z.string().min(1),
2417
+ value: import_zod21.z.unknown(),
2418
+ reason: import_zod21.z.string().min(1)
2419
+ }).strict();
2420
+ var DesignArtifactSchema = import_zod21.z.object({
2421
+ audience: DesignAudienceSchema,
2422
+ voice: DesignVoiceSchema,
2423
+ visual_register: VisualRegisterSchema,
2424
+ constraints: DesignConstraintsSchema,
2425
+ brand_references: BrandReferencesSchema,
2426
+ variances: import_zod21.z.array(DesignVarianceSchema).optional(),
2427
+ /** Markdown body verbatim — attached by the parser, not part of frontmatter. */
2428
+ body: import_zod21.z.string()
2429
+ }).strict();
2430
+ var BEAT_ID_RE = /^[a-z][a-z0-9-]*(\.[a-z0-9][a-z0-9-]*)*$/;
2431
+ var ScriptBeatIdSchema = import_zod22.z.string().regex(BEAT_ID_RE, {
2432
+ message: "beat id must be kebab-case (optionally dot-namespaced, e.g. `proof-1` or `b-roll.intro`)"
2433
+ });
2434
+ var NarratedBeatSchema = import_zod22.z.object({
2435
+ id: ScriptBeatIdSchema,
2436
+ copy: import_zod22.z.string().min(1),
2437
+ intent: import_zod22.z.string().min(1),
2438
+ est_duration_s: import_zod22.z.number().nonnegative(),
2439
+ bind_to_transcript: import_zod22.z.boolean().default(true)
2440
+ }).strict();
2441
+ var NonNarratedBeatSchema = import_zod22.z.object({
2442
+ id: ScriptBeatIdSchema,
2443
+ copy: import_zod22.z.string().min(1),
2444
+ intent: import_zod22.z.string().min(1),
2445
+ est_duration_s: import_zod22.z.number().nonnegative()
2446
+ }).strict();
2447
+ var NarratedBlocksSchema = import_zod22.z.object({
2448
+ hook: import_zod22.z.array(NarratedBeatSchema).min(1),
2449
+ story: import_zod22.z.array(NarratedBeatSchema),
2450
+ proof: import_zod22.z.array(NarratedBeatSchema),
2451
+ cta: import_zod22.z.array(NarratedBeatSchema).min(1)
2452
+ }).strict();
2453
+ var NarratedScriptSchema = import_zod22.z.object({
2454
+ mode: import_zod22.z.literal("narrated"),
2455
+ target_duration_s: import_zod22.z.number().positive(),
2456
+ language: import_zod22.z.string().min(1),
2457
+ /** Optional slot reference for TTS voice (e.g. `voice.host.primary`). */
2458
+ tts_voice: SlotRefSchema.optional(),
2459
+ blocks: NarratedBlocksSchema,
2460
+ /** Markdown body verbatim — attached by the parser when the body has stray prose. */
2461
+ body: import_zod22.z.string()
2462
+ }).strict();
2463
+ var CarouselScriptSchema = import_zod22.z.object({
2464
+ mode: import_zod22.z.literal("carousel"),
2465
+ target_duration_s: import_zod22.z.number().positive(),
2466
+ language: import_zod22.z.string().min(1),
2467
+ tts_voice: SlotRefSchema.optional(),
2468
+ beats: import_zod22.z.array(NonNarratedBeatSchema).min(1),
2469
+ body: import_zod22.z.string()
2470
+ }).strict();
2471
+ var TextOnlyScriptSchema = import_zod22.z.object({
2472
+ mode: import_zod22.z.literal("text-only"),
2473
+ target_duration_s: import_zod22.z.number().positive(),
2474
+ language: import_zod22.z.string().min(1),
2475
+ tts_voice: SlotRefSchema.optional(),
2476
+ beats: import_zod22.z.array(NonNarratedBeatSchema).min(1),
2477
+ body: import_zod22.z.string()
2478
+ }).strict();
2479
+ var ScriptArtifactSchema = import_zod22.z.discriminatedUnion("mode", [
2480
+ NarratedScriptSchema,
2481
+ CarouselScriptSchema,
2482
+ TextOnlyScriptSchema
2483
+ ]);
2484
+ function allScriptBeats(script) {
2485
+ if (script.mode === "narrated") {
2486
+ return [
2487
+ ...script.blocks.hook,
2488
+ ...script.blocks.story,
2489
+ ...script.blocks.proof,
2490
+ ...script.blocks.cta
2491
+ ];
2492
+ }
2493
+ return script.beats;
2494
+ }
2495
+ var BeatWindowSchema = import_zod23.z.object({
2496
+ start_s: import_zod23.z.number().nonnegative(),
2497
+ end_s: import_zod23.z.number().nonnegative()
2498
+ }).strict().refine((w) => w.end_s > w.start_s, {
2499
+ message: "window.end_s must be > window.start_s"
2500
+ });
2501
+ var CameraSchema = import_zod23.z.object({
2502
+ kind: import_zod23.z.enum(["static", "drift", "push", "pull", "whip"]),
2503
+ subject: import_zod23.z.string().min(1).optional()
2504
+ }).strict();
2505
+ var MoodSchema = import_zod23.z.string().min(1);
2506
+ var TransitionKindSchema = import_zod23.z.enum(["cut", "dip-to-color", "whip", "crossfade"]);
2507
+ var TransitionOutSchema = import_zod23.z.object({
2508
+ kind: TransitionKindSchema,
2509
+ duration_s: import_zod23.z.number().nonnegative().optional(),
2510
+ color: import_zod23.z.string().min(1).optional()
2511
+ }).strict();
2512
+ var SfxEntrySchema = import_zod23.z.object({
2513
+ slot: SlotRefSchema,
2514
+ gain_db: import_zod23.z.number()
2515
+ }).strict();
2516
+ var StoryboardBeatSchema = import_zod23.z.object({
2517
+ id: ScriptBeatIdSchema,
2518
+ window: BeatWindowSchema,
2519
+ mood: import_zod23.z.array(MoodSchema).min(1),
2520
+ camera: CameraSchema,
2521
+ slots: import_zod23.z.array(SlotRefSchema),
2522
+ techniques: import_zod23.z.array(import_zod23.z.string().min(1)),
2523
+ transition_out: TransitionOutSchema,
2524
+ sfx: import_zod23.z.array(SfxEntrySchema).optional()
2525
+ }).strict();
2526
+ var StoryboardAspectRatioSchema = import_zod23.z.enum(["9:16", "1:1", "16:9", "4:5"]);
2527
+ var StoryboardArtifactSchema = import_zod23.z.object({
2528
+ technique_library_version: import_zod23.z.number().int().positive(),
2529
+ aspect_ratio: StoryboardAspectRatioSchema,
2530
+ beats: import_zod23.z.array(StoryboardBeatSchema).min(1)
2531
+ }).strict();
2532
+ function isBRollBeatId(id) {
2533
+ return id.startsWith("b-roll.");
2534
+ }
2535
+ var FRONTMATTER_FENCE = "---";
2536
+ function splitFrontmatter(raw) {
2537
+ const text = raw.replace(/\r\n/g, "\n").replace(/^/, "");
2538
+ const lines = text.split("\n");
2539
+ let i = 0;
2540
+ while (i < lines.length && lines[i].trim() === "") i++;
2541
+ if (i >= lines.length || lines[i].trim() !== FRONTMATTER_FENCE) {
2542
+ throw new Error("artifact missing leading `---` frontmatter fence");
2543
+ }
2544
+ const startLine = i + 1;
2545
+ let endLine = -1;
2546
+ for (let j = startLine; j < lines.length; j++) {
2547
+ if (lines[j].trim() === FRONTMATTER_FENCE) {
2548
+ endLine = j;
2549
+ break;
2550
+ }
2551
+ }
2552
+ if (endLine === -1) {
2553
+ throw new Error("artifact missing closing `---` frontmatter fence");
2554
+ }
2555
+ const frontmatterText = lines.slice(startLine, endLine).join("\n");
2556
+ const body = lines.slice(endLine + 1).join("\n").replace(/^\n+/, "");
2557
+ let frontmatter;
2558
+ try {
2559
+ frontmatter = frontmatterText.trim().length === 0 ? {} : (0, import_yaml2.parse)(frontmatterText);
2560
+ } catch (err) {
2561
+ throw new Error(`frontmatter YAML parse error: ${err.message}`);
2562
+ }
2563
+ if (frontmatter === null || typeof frontmatter !== "object" || Array.isArray(frontmatter)) {
2564
+ throw new Error("frontmatter must be a YAML mapping");
2565
+ }
2566
+ return { frontmatter, body };
2567
+ }
2568
+ function parseDesign(raw) {
2569
+ const { frontmatter, body } = splitFrontmatter(raw);
2570
+ return DesignArtifactSchema.parse({ ...frontmatter, body });
2571
+ }
2572
+ var NARRATED_SECTION_HEADERS = {
2573
+ hook: "hook",
2574
+ story: "story",
2575
+ proof: "proof",
2576
+ cta: "cta"
2577
+ };
2578
+ function parseScriptBody(body, mode) {
2579
+ const text = body.replace(/\r\n/g, "\n");
2580
+ const lines = text.split("\n");
2581
+ if (mode === "narrated") {
2582
+ const blocks = {
2583
+ hook: [],
2584
+ story: [],
2585
+ proof: [],
2586
+ cta: []
2587
+ };
2588
+ let currentKey = null;
2589
+ let buffer = [];
2590
+ const remainder2 = [];
2591
+ const flush = () => {
2592
+ if (currentKey === null) return;
2593
+ const yamlText2 = buffer.join("\n").trim();
2594
+ if (yamlText2.length === 0) return;
2595
+ const parsed = (0, import_yaml2.parse)(yamlText2);
2596
+ if (!Array.isArray(parsed)) {
2597
+ throw new Error(`SCRIPT section "${currentKey}" must be a YAML list of beats`);
2598
+ }
2599
+ blocks[currentKey] = parsed;
2600
+ };
2601
+ for (const line of lines) {
2602
+ const headingMatch = /^#\s+(.+?)\s*$/.exec(line);
2603
+ if (headingMatch) {
2604
+ const key = headingMatch[1].toLowerCase();
2605
+ if (key in NARRATED_SECTION_HEADERS) {
2606
+ flush();
2607
+ currentKey = NARRATED_SECTION_HEADERS[key];
2608
+ buffer = [];
2609
+ continue;
2610
+ }
2611
+ }
2612
+ if (currentKey === null) {
2613
+ remainder2.push(line);
2614
+ } else {
2615
+ buffer.push(line);
2616
+ }
2617
+ }
2618
+ flush();
2619
+ return { blocks, remainder: remainder2.join("\n").trim() };
2620
+ }
2621
+ const beatLines = [];
2622
+ const remainder = [];
2623
+ let inBeats = false;
2624
+ let sawAnyHeading = false;
2625
+ for (const line of lines) {
2626
+ const headingMatch = /^#\s+(.+?)\s*$/.exec(line);
2627
+ if (headingMatch) {
2628
+ sawAnyHeading = true;
2629
+ inBeats = headingMatch[1].toLowerCase() === "beats";
2630
+ continue;
2631
+ }
2632
+ if (sawAnyHeading) {
2633
+ if (inBeats) beatLines.push(line);
2634
+ else remainder.push(line);
2635
+ } else {
2636
+ beatLines.push(line);
2637
+ }
2638
+ }
2639
+ const yamlText = beatLines.join("\n").trim();
2640
+ let beats = [];
2641
+ if (yamlText.length > 0) {
2642
+ const parsed = (0, import_yaml2.parse)(yamlText);
2643
+ if (!Array.isArray(parsed)) {
2644
+ throw new Error(`SCRIPT body must be a YAML list of beats (mode: ${mode})`);
2645
+ }
2646
+ beats = parsed;
2647
+ }
2648
+ return { beats, remainder: remainder.join("\n").trim() };
2649
+ }
2650
+ function parseScript(raw) {
2651
+ const { frontmatter, body } = splitFrontmatter(raw);
2652
+ const mode = frontmatter.mode;
2653
+ if (mode !== "narrated" && mode !== "carousel" && mode !== "text-only") {
2654
+ throw new Error(`SCRIPT frontmatter "mode" must be one of narrated | carousel | text-only (got ${JSON.stringify(mode)})`);
2655
+ }
2656
+ const parsed = parseScriptBody(body, mode);
2657
+ const assembled = { ...frontmatter, body: parsed.remainder };
2658
+ if (mode === "narrated") {
2659
+ assembled.blocks = parsed.blocks;
2660
+ } else {
2661
+ assembled.beats = parsed.beats;
2662
+ }
2663
+ return ScriptArtifactSchema.parse(assembled);
2664
+ }
2665
+ function parseStoryboardBody(body) {
2666
+ const text = body.replace(/\r\n/g, "\n");
2667
+ const lines = text.split("\n");
2668
+ const beats = [];
2669
+ let current = null;
2670
+ for (const line of lines) {
2671
+ const headingMatch = /^##\s+(.+?)\s*$/.exec(line);
2672
+ if (headingMatch) {
2673
+ if (current !== null) beats.push(current);
2674
+ current = { id: headingMatch[1].trim(), buffer: [] };
2675
+ continue;
2676
+ }
2677
+ if (current !== null) {
2678
+ current.buffer.push(line);
2679
+ }
2680
+ }
2681
+ if (current !== null) beats.push(current);
2682
+ return beats.map(({ id, buffer }) => {
2683
+ const yamlText = buffer.join("\n").trim();
2684
+ if (yamlText.length === 0) {
2685
+ return { id };
2686
+ }
2687
+ const parsed = (0, import_yaml2.parse)(yamlText);
2688
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
2689
+ throw new Error(`STORYBOARD beat "${id}" body must be a YAML mapping`);
2690
+ }
2691
+ return { id, ...parsed };
2692
+ });
2693
+ }
2694
+ function parseStoryboard(raw) {
2695
+ const { frontmatter, body } = splitFrontmatter(raw);
2696
+ const beats = parseStoryboardBody(body);
2697
+ return StoryboardArtifactSchema.parse({ ...frontmatter, beats });
2698
+ }
2699
+ function validateArtifactSet(set) {
2700
+ const warnings = [];
2701
+ const errors = [];
2702
+ if (set.storyboard && set.script) {
2703
+ const scriptIds = new Set(allScriptBeats(set.script).map((b) => b.id));
2704
+ for (const beat of set.storyboard.beats) {
2705
+ if (isBRollBeatId(beat.id)) continue;
2706
+ if (!scriptIds.has(beat.id)) {
2707
+ errors.push(
2708
+ `STORYBOARD beat "${beat.id}" has no matching SCRIPT beat (and is not prefixed b-roll.)`
2709
+ );
2710
+ }
2711
+ }
2712
+ }
2713
+ if (set.storyboard) {
2714
+ for (const beat of set.storyboard.beats) {
2715
+ for (const slot of beat.slots) {
2716
+ const parsed = parseSlotRef(slot);
2717
+ if (parsed === null) {
2718
+ errors.push(
2719
+ `STORYBOARD beat "${beat.id}" slot "${slot}" is malformed or has an unknown kind (expected one of ${SLOT_KINDS.join(", ")})`
2720
+ );
2721
+ }
2722
+ }
2723
+ if (beat.sfx) {
2724
+ for (const sfx of beat.sfx) {
2725
+ const parsed = parseSlotRef(sfx.slot);
2726
+ if (parsed === null) {
2727
+ errors.push(
2728
+ `STORYBOARD beat "${beat.id}" sfx slot "${sfx.slot}" is malformed or has an unknown kind`
2729
+ );
2730
+ }
2731
+ }
2732
+ }
2733
+ }
2734
+ }
2735
+ if (set.storyboard) {
2736
+ const lib = getTechniqueLibrary(set.storyboard.technique_library_version);
2737
+ if (lib === null) {
2738
+ errors.push(
2739
+ `STORYBOARD references unknown technique_library_version ${set.storyboard.technique_library_version}`
2740
+ );
2741
+ } else {
2742
+ const known = new Set(lib.techniques);
2743
+ for (const beat of set.storyboard.beats) {
2744
+ for (const t of beat.techniques) {
2745
+ if (!known.has(t)) {
2746
+ errors.push(
2747
+ `STORYBOARD beat "${beat.id}" uses unknown technique "${t}" (technique_library_version=${lib.version})`
2748
+ );
2749
+ }
2750
+ }
2751
+ }
2752
+ }
2753
+ }
2754
+ if (set.script) {
2755
+ const sum = allScriptBeats(set.script).reduce((acc, b) => acc + b.est_duration_s, 0);
2756
+ if (sum > set.script.target_duration_s) {
2757
+ warnings.push(
2758
+ `SCRIPT beat est_duration_s sum (${sum.toFixed(2)}s) exceeds target_duration_s (${set.script.target_duration_s}s) \u2014 warn only pre-VO; will hard-fail post-transcript binding`
2759
+ );
2760
+ }
2761
+ }
2762
+ return { ok: errors.length === 0, warnings, errors };
2763
+ }
2764
+ var WorkspaceManifestSchema = import_zod24.z.object({
2765
+ /** Manifest schema version. Pinned to '1.0' for the initial release. */
2766
+ version: import_zod24.z.literal("1.0"),
2767
+ /** Display name; defaults to the workspace dir basename at create time. */
2768
+ name: import_zod24.z.string().min(1),
2769
+ /** ISO 8601 timestamp set by the writer. */
2770
+ created: import_zod24.z.string().min(1),
2771
+ /**
2772
+ * Inherited by new projects created via `atelier init` inside this
2773
+ * workspace. The creator pre-declares the autonomy posture once at the
2774
+ * workspace level; per-project overrides still allowed via per-project
2775
+ * `.atelier/learning-mode.yaml` (writes always happen at workspace level
2776
+ * unless explicitly overridden).
2777
+ */
2778
+ default_mode: import_zod24.z.enum(["ambient", "explicit"]),
2779
+ /**
2780
+ * Relative paths (single-segment subdir names) of known Projects inside
2781
+ * the workspace. Advisory only — on-disk `project.atelier` presence is
2782
+ * authoritative. Refresh via `listProjects(workspaceDir)`.
2783
+ */
2784
+ projects: import_zod24.z.array(import_zod24.z.string().min(1)).optional(),
2785
+ /** Free-text creator description. Optional. */
2786
+ description: import_zod24.z.string().optional()
2787
+ }).strict();
2788
+ var NonEmptyStringListSchema = import_zod25.z.array(import_zod25.z.string().min(1));
2789
+ var MediaNotesConstraintsSchema = import_zod25.z.object({
2790
+ /**
2791
+ * Hard exclusions. Lux + Iris MUST refuse to bind this media to a slot if
2792
+ * any do_not rule applies (the rule is a free-text string the agent reads
2793
+ * with judgement — e.g. "never use in a CTA beat", "no client logos").
2794
+ */
2795
+ do_not: NonEmptyStringListSchema.optional(),
2796
+ /**
2797
+ * Soft preferences — weight up candidacy when these conditions hold. Same
2798
+ * free-text shape as do_not but the polarity is positive ("good for hooks",
2799
+ * "prefer for warm tone").
2800
+ */
2801
+ prefer_for: NonEmptyStringListSchema.optional()
2802
+ }).strict();
2803
+ var MediaNotesFrontmatterSchema = import_zod25.z.object({
2804
+ /** Basename of the media file this notes file sits next to. */
2805
+ file: import_zod25.z.string().min(1),
2806
+ /** ISO 8601 timestamp captured at ingest. */
2807
+ added: import_zod25.z.string().min(1),
2808
+ /** User identifier (env $USER at ingest, "creator" fallback). */
2809
+ added_by: import_zod25.z.string().min(1),
2810
+ /** Free-form labels. Empty by default; agents and creator both append. */
2811
+ tags: NonEmptyStringListSchema.default([]),
2812
+ /** Optional do_not / prefer_for object. Absent means "no rules yet." */
2813
+ constraints: MediaNotesConstraintsSchema.optional()
2814
+ }).strict();
2815
+ var MediaNotesSchema = import_zod25.z.object({
2816
+ frontmatter: MediaNotesFrontmatterSchema,
2817
+ body: import_zod25.z.string()
2818
+ }).strict();
2819
+ var MediaNotesAuthorSchema = import_zod25.z.enum(["lux", "quill", "iris", "creator"]);
2098
2820
 
2099
2821
  // src/commands/validate.ts
2100
2822
  init_dist2();
@@ -2328,21 +3050,78 @@ function stillCommand(program) {
2328
3050
  }
2329
3051
 
2330
3052
  // src/commands/render.ts
2331
- var import_node_fs4 = require("fs");
2332
- var import_node_path4 = require("path");
3053
+ var import_node_fs5 = require("fs");
3054
+ var import_node_path5 = require("path");
2333
3055
 
2334
3056
  // src/commands/render-pipeline.ts
2335
3057
  var import_node_child_process = require("child_process");
3058
+ var import_node_fs4 = require("fs");
3059
+ var import_node_path4 = require("path");
2336
3060
  init_dist2();
2337
3061
  init_dist3();
2338
3062
  async function checkFfmpeg() {
2339
- return new Promise((resolve14) => {
3063
+ return new Promise((resolve28) => {
2340
3064
  const proc = (0, import_node_child_process.spawn)("ffmpeg", ["-version"], { stdio: "pipe" });
2341
- proc.on("error", () => resolve14(false));
2342
- proc.on("close", (code) => resolve14(code === 0));
3065
+ proc.on("error", () => resolve28(false));
3066
+ proc.on("close", (code) => resolve28(code === 0));
2343
3067
  });
2344
3068
  }
2345
- function buildFfmpegArgs(width, height, fps, format, output) {
3069
+ function buildFfmpegArgs(width, height, fps, format, output, sourceVideoPath) {
3070
+ if (sourceVideoPath && format === "mp4") {
3071
+ return [
3072
+ "-y",
3073
+ // Input 0: source video (+ audio). ffmpeg auto-decodes via libavformat.
3074
+ "-i",
3075
+ sourceVideoPath,
3076
+ // Input 1: canvas overlay frames on stdin. Same dimensions + fps as
3077
+ // the doc so the overlay aligns frame-for-frame with input 0.
3078
+ "-f",
3079
+ "rawvideo",
3080
+ "-pix_fmt",
3081
+ "bgra",
3082
+ "-s",
3083
+ `${width}x${height}`,
3084
+ "-r",
3085
+ String(fps),
3086
+ "-i",
3087
+ "pipe:0",
3088
+ // Filter graph:
3089
+ // 1. Scale + pad the source video to the doc's canvas dimensions
3090
+ // with `force_original_aspect_ratio=decrease` (letterbox-fit, no
3091
+ // crop) so a 4K iPhone source rendered into a 1080x1920 doc
3092
+ // respects the doc's intent instead of upscaling everything to
3093
+ // source dimensions. The pad fills gutters with black if the
3094
+ // source aspect differs from the canvas aspect.
3095
+ // 2. Overlay the caption canvas on top. Canvas frames must have
3096
+ // transparent backgrounds — renderFrame's clearRect (fixed
3097
+ // 2026-05-28) gives true transparency by default.
3098
+ "-filter_complex",
3099
+ `[0:v]scale=${width}:${height}:force_original_aspect_ratio=decrease,pad=${width}:${height}:(ow-iw)/2:(oh-ih)/2:color=black[bg];[bg][1:v]overlay=0:0[v]`,
3100
+ "-map",
3101
+ "[v]",
3102
+ // Copy the source audio track verbatim. No re-encode.
3103
+ "-map",
3104
+ "0:a?",
3105
+ "-c:v",
3106
+ "libx264",
3107
+ "-pix_fmt",
3108
+ "yuv420p",
3109
+ "-preset",
3110
+ "medium",
3111
+ "-crf",
3112
+ "18",
3113
+ "-c:a",
3114
+ "copy",
3115
+ // `-shortest` so the output ends with whichever stream finishes first
3116
+ // (typically the source video — we render exactly that many canvas
3117
+ // frames). Without it, if the canvas had a few extra frames the
3118
+ // output would have a silent tail.
3119
+ "-shortest",
3120
+ "-movflags",
3121
+ "+faststart",
3122
+ output
3123
+ ];
3124
+ }
2346
3125
  const input = [
2347
3126
  "-y",
2348
3127
  "-f",
@@ -2381,6 +3160,21 @@ function buildFfmpegArgs(width, height, fps, format, output) {
2381
3160
  output
2382
3161
  ];
2383
3162
  }
3163
+ function findPrimaryVideoSource(doc, docPath) {
3164
+ for (const layer of doc.layers) {
3165
+ if (layer.visual.type !== "video") continue;
3166
+ const v = layer.visual;
3167
+ const src = v.src ?? (v.assetId ? doc.assets?.[v.assetId]?.src : void 0);
3168
+ if (!src) continue;
3169
+ if ((0, import_node_path4.isAbsolute)(src)) {
3170
+ return (0, import_node_fs4.existsSync)(src) ? src : null;
3171
+ }
3172
+ if (!docPath) return null;
3173
+ const absolute = (0, import_node_path4.resolve)((0, import_node_path4.dirname)(docPath), src);
3174
+ return (0, import_node_fs4.existsSync)(absolute) ? absolute : null;
3175
+ }
3176
+ return null;
3177
+ }
2384
3178
  async function preloadImages(doc, loadImage2) {
2385
3179
  const sources = /* @__PURE__ */ new Set();
2386
3180
  for (const layer of doc.layers) {
@@ -2419,7 +3213,7 @@ async function preloadImages(doc, loadImage2) {
2419
3213
  for (const src of preloaded.keys()) {
2420
3214
  imageCache.load(src);
2421
3215
  }
2422
- await new Promise((resolve14) => process.nextTick(resolve14));
3216
+ await new Promise((resolve28) => process.nextTick(resolve28));
2423
3217
  return imageCache;
2424
3218
  }
2425
3219
  async function renderDocument(doc, opts) {
@@ -2464,7 +3258,8 @@ async function renderDocument(doc, opts) {
2464
3258
  const imageCache = await preloadImages(doc, loadImage2);
2465
3259
  const canvas = createCanvas2(width, height);
2466
3260
  const ctx = canvas.getContext("2d");
2467
- const ffmpegArgs = buildFfmpegArgs(width, height, fps, format, output);
3261
+ const sourceVideoPath = findPrimaryVideoSource(doc, opts.docPath) ?? void 0;
3262
+ const ffmpegArgs = buildFfmpegArgs(width, height, fps, format, output, sourceVideoPath);
2468
3263
  const ffmpeg = (0, import_node_child_process.spawn)("ffmpeg", ffmpegArgs, {
2469
3264
  stdio: ["pipe", "pipe", "pipe"]
2470
3265
  });
@@ -2483,7 +3278,7 @@ async function renderDocument(doc, opts) {
2483
3278
  const canWrite = ffmpeg.stdin.write(raw);
2484
3279
  if (!canWrite) {
2485
3280
  await new Promise(
2486
- (resolve14) => ffmpeg.stdin.once("drain", resolve14)
3281
+ (resolve28) => ffmpeg.stdin.once("drain", resolve28)
2487
3282
  );
2488
3283
  }
2489
3284
  frameIndex++;
@@ -2496,8 +3291,8 @@ async function renderDocument(doc, opts) {
2496
3291
  }
2497
3292
  }
2498
3293
  ffmpeg.stdin.end();
2499
- const exitCode = await new Promise((resolve14) => {
2500
- ffmpeg.on("close", resolve14);
3294
+ const exitCode = await new Promise((resolve28) => {
3295
+ ffmpeg.on("close", resolve28);
2501
3296
  });
2502
3297
  if (exitCode !== 0) {
2503
3298
  throw new Error(
@@ -2516,10 +3311,10 @@ ${stderrOutput.slice(-500)}`
2516
3311
 
2517
3312
  // src/commands/render.ts
2518
3313
  function readAndParse3(file) {
2519
- const absPath = (0, import_node_path4.resolve)(file);
3314
+ const absPath = (0, import_node_path5.resolve)(file);
2520
3315
  let content;
2521
3316
  try {
2522
- content = (0, import_node_fs4.readFileSync)(absPath, "utf-8");
3317
+ content = (0, import_node_fs5.readFileSync)(absPath, "utf-8");
2523
3318
  } catch {
2524
3319
  console.error(`Cannot read file: ${absPath}`);
2525
3320
  return process.exit(1);
@@ -2536,7 +3331,7 @@ function readAndParse3(file) {
2536
3331
  }
2537
3332
  function inferFormat(output) {
2538
3333
  if (!output) return "mp4";
2539
- const ext = (0, import_node_path4.extname)(output).toLowerCase();
3334
+ const ext = (0, import_node_path5.extname)(output).toLowerCase();
2540
3335
  if (ext === ".gif") return "gif";
2541
3336
  return "mp4";
2542
3337
  }
@@ -2570,14 +3365,20 @@ function renderCommand(program) {
2570
3365
  } else {
2571
3366
  format = inferFormat(options.output);
2572
3367
  }
2573
- const inputName = (0, import_node_path4.basename)(file, (0, import_node_path4.extname)(file));
3368
+ const inputName = (0, import_node_path5.basename)(file, (0, import_node_path5.extname)(file));
2574
3369
  const output = options.output ?? `${inputName}.${format}`;
2575
3370
  const startTime = Date.now();
2576
3371
  try {
2577
3372
  const result = await renderDocument(doc, {
2578
- output: (0, import_node_path4.resolve)(output),
3373
+ output: (0, import_node_path5.resolve)(output),
2579
3374
  format,
2580
3375
  states: options.state,
3376
+ // Absolute path of the input doc — the pipeline uses this to
3377
+ // resolve relative `media/...` paths in VideoVisual layers so
3378
+ // ffmpeg can read the source video as a second input (overlay
3379
+ // base + audio passthrough). Without this, render falls back to
3380
+ // canvas-only mode — no source pixels, no audio.
3381
+ docPath: (0, import_node_path5.resolve)(file),
2581
3382
  onProgress: ({ frame, totalFrames, state, percent }) => {
2582
3383
  const elapsed = (Date.now() - startTime) / 1e3;
2583
3384
  const rate = elapsed > 0 ? frame / elapsed : 0;
@@ -2601,8 +3402,8 @@ function renderCommand(program) {
2601
3402
  }
2602
3403
 
2603
3404
  // src/commands/export-svg.ts
2604
- var import_node_fs5 = require("fs");
2605
- var import_node_path5 = require("path");
3405
+ var import_node_fs6 = require("fs");
3406
+ var import_node_path6 = require("path");
2606
3407
 
2607
3408
  // ../svg/dist/index.js
2608
3409
  init_dist2();
@@ -3067,10 +3868,10 @@ function renderRefSVG(eff, visual, _parentDoc, opts, _depth, _visitedRefs) {
3067
3868
 
3068
3869
  // src/commands/export-svg.ts
3069
3870
  function readAndParse4(file) {
3070
- const absPath = (0, import_node_path5.resolve)(file);
3871
+ const absPath = (0, import_node_path6.resolve)(file);
3071
3872
  let content;
3072
3873
  try {
3073
- content = (0, import_node_fs5.readFileSync)(absPath, "utf-8");
3874
+ content = (0, import_node_fs6.readFileSync)(absPath, "utf-8");
3074
3875
  } catch {
3075
3876
  console.error(`Cannot read file: ${absPath}`);
3076
3877
  return process.exit(1);
@@ -3112,7 +3913,7 @@ function exportSvgCommand(program) {
3112
3913
  xmlDeclaration: options.xmlDeclaration
3113
3914
  });
3114
3915
  if (options.output) {
3115
- (0, import_node_fs5.writeFileSync)((0, import_node_path5.resolve)(options.output), svg, "utf-8");
3916
+ (0, import_node_fs6.writeFileSync)((0, import_node_path6.resolve)(options.output), svg, "utf-8");
3116
3917
  } else {
3117
3918
  console.log(svg);
3118
3919
  }
@@ -3125,8 +3926,8 @@ function exportSvgCommand(program) {
3125
3926
  }
3126
3927
 
3127
3928
  // src/commands/export-lottie.ts
3128
- var import_node_fs6 = require("fs");
3129
- var import_node_path6 = require("path");
3929
+ var import_node_fs7 = require("fs");
3930
+ var import_node_path7 = require("path");
3130
3931
 
3131
3932
  // ../lottie/dist/index.js
3132
3933
  function colorToLottie(color) {
@@ -3620,8 +4421,11 @@ function mapBlendMode(mode) {
3620
4421
  }
3621
4422
  function collectUnsupportedWarnings(doc, state) {
3622
4423
  const warnings = [];
3623
- if (state.audio) {
3624
- warnings.push("Audio tracks are not supported in Lottie format and will be dropped");
4424
+ const audioLayers = doc.layers.filter((l) => l.visual.type === "audio");
4425
+ if (audioLayers.length > 0) {
4426
+ warnings.push(
4427
+ `${audioLayers.length} audio layer(s) are not supported in Lottie format and will be dropped`
4428
+ );
3625
4429
  }
3626
4430
  for (const delta of state.deltas) {
3627
4431
  if (isExpression22(delta.from) || isExpression22(delta.to)) {
@@ -3695,10 +4499,10 @@ function exportToLottie(doc, opts) {
3695
4499
 
3696
4500
  // src/commands/export-lottie.ts
3697
4501
  function readAndParse5(file) {
3698
- const absPath = (0, import_node_path6.resolve)(file);
4502
+ const absPath = (0, import_node_path7.resolve)(file);
3699
4503
  let content;
3700
4504
  try {
3701
- content = (0, import_node_fs6.readFileSync)(absPath, "utf-8");
4505
+ content = (0, import_node_fs7.readFileSync)(absPath, "utf-8");
3702
4506
  } catch {
3703
4507
  console.error(`Cannot read file: ${absPath}`);
3704
4508
  return process.exit(1);
@@ -3726,7 +4530,7 @@ function exportLottieCommand(program) {
3726
4530
  }
3727
4531
  const output = JSON.stringify(json, null, 2);
3728
4532
  if (options.output) {
3729
- (0, import_node_fs6.writeFileSync)((0, import_node_path6.resolve)(options.output), output, "utf-8");
4533
+ (0, import_node_fs7.writeFileSync)((0, import_node_path7.resolve)(options.output), output, "utf-8");
3730
4534
  } else {
3731
4535
  console.log(output);
3732
4536
  }
@@ -3739,8 +4543,8 @@ function exportLottieCommand(program) {
3739
4543
  }
3740
4544
 
3741
4545
  // src/commands/export-image.ts
3742
- var import_node_fs7 = require("fs");
3743
- var import_node_path7 = require("path");
4546
+ var import_node_fs8 = require("fs");
4547
+ var import_node_path8 = require("path");
3744
4548
 
3745
4549
  // src/lib/render-image.ts
3746
4550
  init_dist2();
@@ -3833,7 +4637,7 @@ async function preloadImages2(doc, loadImage2) {
3833
4637
  }
3834
4638
  });
3835
4639
  for (const src of preloaded.keys()) imageCache.load(src);
3836
- await new Promise((resolve14) => process.nextTick(resolve14));
4640
+ await new Promise((resolve28) => process.nextTick(resolve28));
3837
4641
  return { imageCache, loaded: preloaded };
3838
4642
  }
3839
4643
  async function renderDocumentToPng(doc, opts = {}) {
@@ -3891,10 +4695,10 @@ async function renderDocumentToPng(doc, opts = {}) {
3891
4695
 
3892
4696
  // src/commands/export-image.ts
3893
4697
  function readAndParse6(file) {
3894
- const absPath = (0, import_node_path7.resolve)(file);
4698
+ const absPath = (0, import_node_path8.resolve)(file);
3895
4699
  let content;
3896
4700
  try {
3897
- content = (0, import_node_fs7.readFileSync)(absPath, "utf-8");
4701
+ content = (0, import_node_fs8.readFileSync)(absPath, "utf-8");
3898
4702
  } catch {
3899
4703
  console.error(`Cannot read file: ${absPath}`);
3900
4704
  return process.exit(1);
@@ -3938,7 +4742,7 @@ function exportImageCommand(program) {
3938
4742
  width,
3939
4743
  height
3940
4744
  });
3941
- (0, import_node_fs7.writeFileSync)((0, import_node_path7.resolve)(options.out), buffer);
4745
+ (0, import_node_fs8.writeFileSync)((0, import_node_path8.resolve)(options.out), buffer);
3942
4746
  } catch (err) {
3943
4747
  if (err instanceof CanvasUnavailableError) {
3944
4748
  console.error(err.message);
@@ -3952,29 +4756,29 @@ function exportImageCommand(program) {
3952
4756
  }
3953
4757
 
3954
4758
  // src/commands/carousel.ts
3955
- var import_node_fs9 = require("fs");
3956
- var import_node_path9 = require("path");
4759
+ var import_node_fs10 = require("fs");
4760
+ var import_node_path10 = require("path");
3957
4761
 
3958
4762
  // src/lib/recipe.ts
3959
- var import_node_fs8 = require("fs");
3960
- var import_node_path8 = require("path");
4763
+ var import_node_fs9 = require("fs");
4764
+ var import_node_path9 = require("path");
3961
4765
  var import_node_os = require("os");
3962
- var import_yaml2 = require("yaml");
4766
+ var import_yaml3 = require("yaml");
3963
4767
  var RECIPE_VERSION = "1.0";
3964
4768
  function resolveRecipePath(pathOrName, projectDir) {
3965
- if ((0, import_node_path8.isAbsolute)(pathOrName) || pathOrName.includes("/") || pathOrName.includes("\\")) {
3966
- return (0, import_node_path8.resolve)(pathOrName);
4769
+ if ((0, import_node_path9.isAbsolute)(pathOrName) || pathOrName.includes("/") || pathOrName.includes("\\")) {
4770
+ return (0, import_node_path9.resolve)(pathOrName);
3967
4771
  }
3968
4772
  const candidates = [];
3969
4773
  const exts = [".recipe.yaml", ".recipe.json", ".yaml", ".yml", ".json"];
3970
4774
  if (projectDir) {
3971
- const projectRecipesDir = (0, import_node_path8.join)((0, import_node_path8.resolve)(projectDir), ".atelier", "recipes");
3972
- for (const ext of exts) candidates.push((0, import_node_path8.join)(projectRecipesDir, `${pathOrName}${ext}`));
4775
+ const projectRecipesDir = (0, import_node_path9.join)((0, import_node_path9.resolve)(projectDir), ".atelier", "recipes");
4776
+ for (const ext of exts) candidates.push((0, import_node_path9.join)(projectRecipesDir, `${pathOrName}${ext}`));
3973
4777
  }
3974
- const userRecipesDir = (0, import_node_path8.join)((0, import_node_os.homedir)(), ".atelier", "recipes");
3975
- for (const ext of exts) candidates.push((0, import_node_path8.join)(userRecipesDir, `${pathOrName}${ext}`));
4778
+ const userRecipesDir = (0, import_node_path9.join)((0, import_node_os.homedir)(), ".atelier", "recipes");
4779
+ for (const ext of exts) candidates.push((0, import_node_path9.join)(userRecipesDir, `${pathOrName}${ext}`));
3976
4780
  for (const candidate of candidates) {
3977
- if ((0, import_node_fs8.existsSync)(candidate)) return candidate;
4781
+ if ((0, import_node_fs9.existsSync)(candidate)) return candidate;
3978
4782
  }
3979
4783
  throw new Error(
3980
4784
  `Recipe "${pathOrName}" not found. Looked in:
@@ -3983,12 +4787,12 @@ ${candidates.map((c) => ` ${c}`).join("\n")}`
3983
4787
  }
3984
4788
  function loadRecipe(pathOrName, projectDir) {
3985
4789
  const path = resolveRecipePath(pathOrName, projectDir);
3986
- const raw = (0, import_node_fs8.readFileSync)(path, "utf-8");
4790
+ const raw = (0, import_node_fs9.readFileSync)(path, "utf-8");
3987
4791
  let parsed;
3988
4792
  if (path.endsWith(".json")) {
3989
4793
  parsed = JSON.parse(raw);
3990
4794
  } else {
3991
- parsed = (0, import_yaml2.parse)(raw);
4795
+ parsed = (0, import_yaml3.parse)(raw);
3992
4796
  }
3993
4797
  const result = validateRecipe(parsed);
3994
4798
  if (!result.success) {
@@ -4145,7 +4949,7 @@ function renderRecipeWithDefaults(recipe) {
4145
4949
  };
4146
4950
  }
4147
4951
  function recipeToYaml(recipe) {
4148
- return (0, import_yaml2.stringify)(recipe);
4952
+ return (0, import_yaml3.stringify)(recipe);
4149
4953
  }
4150
4954
  var DEFAULT_OVERLAY_MARGIN = 24;
4151
4955
  var DEFAULT_OVERLAY_TEXT_STYLE = {
@@ -4256,45 +5060,45 @@ function renderPageNumberFormat(format, currentIndex, totalCount) {
4256
5060
  var IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".webp"]);
4257
5061
  var DEFAULT_CANVAS = 1080;
4258
5062
  function expandInputs(pattern) {
4259
- const abs = (0, import_node_path9.resolve)(pattern);
5063
+ const abs = (0, import_node_path10.resolve)(pattern);
4260
5064
  let isDir = false;
4261
5065
  try {
4262
- isDir = (0, import_node_fs9.statSync)(abs).isDirectory();
5066
+ isDir = (0, import_node_fs10.statSync)(abs).isDirectory();
4263
5067
  } catch {
4264
5068
  isDir = false;
4265
5069
  }
4266
5070
  if (isDir) {
4267
5071
  return listImages(abs);
4268
5072
  }
4269
- const dir = (0, import_node_path9.dirname)(abs);
4270
- const base = (0, import_node_path9.basename)(abs);
5073
+ const dir = (0, import_node_path10.dirname)(abs);
5074
+ const base = (0, import_node_path10.basename)(abs);
4271
5075
  if (base.includes("*")) {
4272
5076
  const matcher = globToRegExp(base);
4273
5077
  let entries;
4274
5078
  try {
4275
- entries = (0, import_node_fs9.readdirSync)(dir);
5079
+ entries = (0, import_node_fs10.readdirSync)(dir);
4276
5080
  } catch {
4277
5081
  return [];
4278
5082
  }
4279
- return entries.filter((name) => matcher.test(name) && isImageFile(name)).map((name) => (0, import_node_path9.join)(dir, name)).sort();
5083
+ return entries.filter((name) => matcher.test(name) && isImageFile(name)).map((name) => (0, import_node_path10.join)(dir, name)).sort();
4280
5084
  }
4281
5085
  return isImageFile(base) ? [abs] : [];
4282
5086
  }
4283
5087
  function listImages(dir) {
4284
5088
  let entries;
4285
5089
  try {
4286
- entries = (0, import_node_fs9.readdirSync)(dir);
5090
+ entries = (0, import_node_fs10.readdirSync)(dir);
4287
5091
  } catch {
4288
5092
  return [];
4289
5093
  }
4290
- return entries.filter(isImageFile).map((name) => (0, import_node_path9.join)(dir, name)).sort();
5094
+ return entries.filter(isImageFile).map((name) => (0, import_node_path10.join)(dir, name)).sort();
4291
5095
  }
4292
5096
  function isImageFile(name) {
4293
- return IMAGE_EXTS.has((0, import_node_path9.extname)(name).toLowerCase());
5097
+ return IMAGE_EXTS.has((0, import_node_path10.extname)(name).toLowerCase());
4294
5098
  }
4295
5099
  function globToRegExp(glob) {
4296
5100
  const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&");
4297
- const pattern = escaped.replace(/\*/g, `[^${import_node_path9.sep === "\\" ? "\\\\" : import_node_path9.sep}]*`).replace(/\?/g, ".");
5101
+ const pattern = escaped.replace(/\*/g, `[^${import_node_path10.sep === "\\" ? "\\\\" : import_node_path10.sep}]*`).replace(/\?/g, ".");
4298
5102
  return new RegExp(`^${pattern}$`);
4299
5103
  }
4300
5104
  function composeCarouselFrameDoc(args) {
@@ -4324,8 +5128,8 @@ function composeCarouselFrameDoc(args) {
4324
5128
  function carouselFileName(index, total, imagePath) {
4325
5129
  const padWidth = Math.max(2, String(total).length);
4326
5130
  const prefix = String(index).padStart(padWidth, "0");
4327
- const ext = (0, import_node_path9.extname)(imagePath);
4328
- const stem = (0, import_node_path9.basename)(imagePath, ext);
5131
+ const ext = (0, import_node_path10.extname)(imagePath);
5132
+ const stem = (0, import_node_path10.basename)(imagePath, ext);
4329
5133
  return `${prefix}-${stem}.png`;
4330
5134
  }
4331
5135
  function parseDim2(raw, name) {
@@ -4368,9 +5172,9 @@ function carouselCommand(program) {
4368
5172
  process.exit(1);
4369
5173
  return;
4370
5174
  }
4371
- const outDir = (0, import_node_path9.resolve)(options.outDir);
5175
+ const outDir = (0, import_node_path10.resolve)(options.outDir);
4372
5176
  try {
4373
- (0, import_node_fs9.mkdirSync)(outDir, { recursive: true });
5177
+ (0, import_node_fs10.mkdirSync)(outDir, { recursive: true });
4374
5178
  } catch (err) {
4375
5179
  console.error(`atelier carousel: cannot create out-dir ${outDir}: ${err.message}`);
4376
5180
  process.exit(1);
@@ -4396,8 +5200,8 @@ function carouselCommand(program) {
4396
5200
  refitImageBounds: ({ canvas, natural }) => fitImageToCanvas(canvas, natural)
4397
5201
  });
4398
5202
  const outName = carouselFileName(index, total, imagePath);
4399
- (0, import_node_fs9.writeFileSync)((0, import_node_path9.join)(outDir, outName), buffer);
4400
- console.log(` [${index}/${total}] ${(0, import_node_path9.basename)(imagePath)} \u2192 ${outName}`);
5203
+ (0, import_node_fs10.writeFileSync)((0, import_node_path10.join)(outDir, outName), buffer);
5204
+ console.log(` [${index}/${total}] ${(0, import_node_path10.basename)(imagePath)} \u2192 ${outName}`);
4401
5205
  }
4402
5206
  } catch (err) {
4403
5207
  if (err instanceof CanvasUnavailableError) {
@@ -4415,20 +5219,21 @@ Done. ${total} image${total === 1 ? "" : "s"} \u2192 ${outDir}`);
4415
5219
  }
4416
5220
 
4417
5221
  // src/commands/assets.ts
4418
- var import_node_fs10 = require("fs");
4419
- var import_node_path10 = require("path");
5222
+ var import_node_fs11 = require("fs");
5223
+ var import_node_path11 = require("path");
4420
5224
  function getAssets(doc) {
4421
5225
  const assets = doc.assets ?? {};
4422
5226
  return Object.entries(assets).map(([assetId, asset]) => {
4423
- const usedByLayers = doc.layers.filter((l) => l.visual.type === "image" && l.visual.assetId === assetId).map((l) => l.id);
4424
- const usedByStates = Object.entries(doc.states).filter(([, state]) => state.audio?.src === assetId).map(([name]) => name);
5227
+ const usedByLayers = doc.layers.filter((l) => {
5228
+ const v = l.visual;
5229
+ return (v.type === "image" || v.type === "video" || v.type === "audio") && "assetId" in v && v.assetId === assetId;
5230
+ }).map((l) => l.id);
4425
5231
  return {
4426
5232
  assetId,
4427
5233
  type: asset.type,
4428
5234
  src: asset.src,
4429
5235
  description: asset.description,
4430
- usedByLayers,
4431
- usedByStates
5236
+ usedByLayers
4432
5237
  };
4433
5238
  });
4434
5239
  }
@@ -4441,17 +5246,14 @@ function formatAssets(assets) {
4441
5246
  if (a.usedByLayers.length > 0) {
4442
5247
  lines.push(` Layers: ${a.usedByLayers.join(", ")}`);
4443
5248
  }
4444
- if (a.usedByStates.length > 0) {
4445
- lines.push(` States: ${a.usedByStates.join(", ")}`);
4446
- }
4447
5249
  }
4448
5250
  return lines.join("\n");
4449
5251
  }
4450
5252
  function readAndParse7(file) {
4451
- const absPath = (0, import_node_path10.resolve)(file);
5253
+ const absPath = (0, import_node_path11.resolve)(file);
4452
5254
  let content;
4453
5255
  try {
4454
- content = (0, import_node_fs10.readFileSync)(absPath, "utf-8");
5256
+ content = (0, import_node_fs11.readFileSync)(absPath, "utf-8");
4455
5257
  } catch {
4456
5258
  console.error(`Cannot read file: ${absPath}`);
4457
5259
  return process.exit(1);
@@ -4475,8 +5277,8 @@ function assetsCommand(program) {
4475
5277
  }
4476
5278
 
4477
5279
  // src/commands/variables.ts
4478
- var import_node_fs11 = require("fs");
4479
- var import_node_path11 = require("path");
5280
+ var import_node_fs12 = require("fs");
5281
+ var import_node_path12 = require("path");
4480
5282
  init_dist2();
4481
5283
  function getVariables(doc) {
4482
5284
  const variables = doc.variables ?? {};
@@ -4512,10 +5314,10 @@ function formatVariables(info) {
4512
5314
  return lines.join("\n");
4513
5315
  }
4514
5316
  function readAndParse8(file) {
4515
- const absPath = (0, import_node_path11.resolve)(file);
5317
+ const absPath = (0, import_node_path12.resolve)(file);
4516
5318
  let content;
4517
5319
  try {
4518
- content = (0, import_node_fs11.readFileSync)(absPath, "utf-8");
5320
+ content = (0, import_node_fs12.readFileSync)(absPath, "utf-8");
4519
5321
  } catch {
4520
5322
  console.error(`Cannot read file: ${absPath}`);
4521
5323
  return process.exit(1);
@@ -4539,8 +5341,8 @@ function variablesCommand(program) {
4539
5341
  }
4540
5342
 
4541
5343
  // src/lib/video-project.ts
4542
- var import_node_fs12 = require("fs");
4543
- var import_node_path12 = require("path");
5344
+ var import_node_fs13 = require("fs");
5345
+ var import_node_path13 = require("path");
4544
5346
  var VIDEO_PROJECT_VERSION = "1.0";
4545
5347
  var VIDEO_CUTLIST_VERSION = "1.1";
4546
5348
  var VIDEO_TRANSCRIPT_VERSION = "1.1";
@@ -4551,24 +5353,24 @@ function effectiveSpan(cut, duration) {
4551
5353
  };
4552
5354
  }
4553
5355
  async function createVideoProject(srcPath, destDir) {
4554
- const absSrc = (0, import_node_path12.resolve)(srcPath);
4555
- const ext = (0, import_node_path12.extname)(absSrc);
4556
- const stem = (0, import_node_path12.basename)(absSrc, ext);
4557
- const projectDir = destDir ? (0, import_node_path12.resolve)(destDir) : (0, import_node_path12.join)((0, import_node_path12.resolve)(absSrc, ".."), stem);
4558
- if (!(0, import_node_fs12.existsSync)(projectDir)) {
4559
- (0, import_node_fs12.mkdirSync)(projectDir, { recursive: true });
5356
+ const absSrc = (0, import_node_path13.resolve)(srcPath);
5357
+ const ext = (0, import_node_path13.extname)(absSrc);
5358
+ const stem = (0, import_node_path13.basename)(absSrc, ext);
5359
+ const projectDir = destDir ? (0, import_node_path13.resolve)(destDir) : (0, import_node_path13.join)((0, import_node_path13.resolve)(absSrc, ".."), stem);
5360
+ if (!(0, import_node_fs13.existsSync)(projectDir)) {
5361
+ (0, import_node_fs13.mkdirSync)(projectDir, { recursive: true });
4560
5362
  }
4561
5363
  const sourceFilename = `source${ext}`;
4562
- const sourcePath = (0, import_node_path12.join)(projectDir, sourceFilename);
4563
- if (!(0, import_node_fs12.existsSync)(sourcePath)) {
4564
- (0, import_node_fs12.copyFileSync)(absSrc, sourcePath);
5364
+ const sourcePath = (0, import_node_path13.join)(projectDir, sourceFilename);
5365
+ if (!(0, import_node_fs13.existsSync)(sourcePath)) {
5366
+ (0, import_node_fs13.copyFileSync)(absSrc, sourcePath);
4565
5367
  }
4566
- const compositionPath = (0, import_node_path12.join)(projectDir, "project.atelier");
4567
- const transcriptPath = (0, import_node_path12.join)(projectDir, "transcript.json");
4568
- const cutsPath = (0, import_node_path12.join)(projectDir, "cuts.json");
4569
- const exportDir = (0, import_node_path12.join)(projectDir, "export");
4570
- if (!(0, import_node_fs12.existsSync)(exportDir)) {
4571
- (0, import_node_fs12.mkdirSync)(exportDir, { recursive: true });
5368
+ const compositionPath = (0, import_node_path13.join)(projectDir, "project.atelier");
5369
+ const transcriptPath = (0, import_node_path13.join)(projectDir, "transcript.json");
5370
+ const cutsPath = (0, import_node_path13.join)(projectDir, "cuts.json");
5371
+ const exportDir = (0, import_node_path13.join)(projectDir, "export");
5372
+ if (!(0, import_node_fs13.existsSync)(exportDir)) {
5373
+ (0, import_node_fs13.mkdirSync)(exportDir, { recursive: true });
4572
5374
  }
4573
5375
  const manifest = {
4574
5376
  source: sourceFilename,
@@ -4587,7 +5389,7 @@ async function createVideoProject(srcPath, destDir) {
4587
5389
  src: {
4588
5390
  type: "video",
4589
5391
  src: sourceFilename,
4590
- description: `Source: ${(0, import_node_path12.basename)(absSrc)}`
5392
+ description: `Source: ${(0, import_node_path13.basename)(absSrc)}`
4591
5393
  }
4592
5394
  },
4593
5395
  layers: [
@@ -4613,16 +5415,16 @@ async function createVideoProject(srcPath, destDir) {
4613
5415
  }
4614
5416
  }
4615
5417
  };
4616
- if (!(0, import_node_fs12.existsSync)(compositionPath)) {
4617
- (0, import_node_fs12.writeFileSync)(compositionPath, JSON.stringify(draft, null, 2), "utf-8");
5418
+ if (!(0, import_node_fs13.existsSync)(compositionPath)) {
5419
+ (0, import_node_fs13.writeFileSync)(compositionPath, JSON.stringify(draft, null, 2), "utf-8");
4618
5420
  }
4619
5421
  const initialCuts = {
4620
5422
  version: VIDEO_CUTLIST_VERSION,
4621
5423
  source: sourceFilename,
4622
5424
  cuts: []
4623
5425
  };
4624
- if (!(0, import_node_fs12.existsSync)(cutsPath)) {
4625
- (0, import_node_fs12.writeFileSync)(cutsPath, JSON.stringify(initialCuts, null, 2), "utf-8");
5426
+ if (!(0, import_node_fs13.existsSync)(cutsPath)) {
5427
+ (0, import_node_fs13.writeFileSync)(cutsPath, JSON.stringify(initialCuts, null, 2), "utf-8");
4626
5428
  }
4627
5429
  return {
4628
5430
  dir: projectDir,
@@ -4635,11 +5437,11 @@ async function createVideoProject(srcPath, destDir) {
4635
5437
  };
4636
5438
  }
4637
5439
  function loadVideoProject(dir) {
4638
- const projectDir = (0, import_node_path12.resolve)(dir);
5440
+ const projectDir = (0, import_node_path13.resolve)(dir);
4639
5441
  const possibleExts = [".mp4", ".mov", ".webm", ".mkv", ".avi"];
4640
5442
  let sourceFilename = "source.mp4";
4641
5443
  for (const ext of possibleExts) {
4642
- if ((0, import_node_fs12.existsSync)((0, import_node_path12.join)(projectDir, `source${ext}`))) {
5444
+ if ((0, import_node_fs13.existsSync)((0, import_node_path13.join)(projectDir, `source${ext}`))) {
4643
5445
  sourceFilename = `source${ext}`;
4644
5446
  break;
4645
5447
  }
@@ -4655,19 +5457,19 @@ function loadVideoProject(dir) {
4655
5457
  };
4656
5458
  return {
4657
5459
  dir: projectDir,
4658
- sourcePath: (0, import_node_path12.join)(projectDir, sourceFilename),
4659
- compositionPath: (0, import_node_path12.join)(projectDir, "project.atelier"),
4660
- transcriptPath: (0, import_node_path12.join)(projectDir, "transcript.json"),
4661
- cutsPath: (0, import_node_path12.join)(projectDir, "cuts.json"),
4662
- exportDir: (0, import_node_path12.join)(projectDir, "export"),
5460
+ sourcePath: (0, import_node_path13.join)(projectDir, sourceFilename),
5461
+ compositionPath: (0, import_node_path13.join)(projectDir, "project.atelier"),
5462
+ transcriptPath: (0, import_node_path13.join)(projectDir, "transcript.json"),
5463
+ cutsPath: (0, import_node_path13.join)(projectDir, "cuts.json"),
5464
+ exportDir: (0, import_node_path13.join)(projectDir, "export"),
4663
5465
  manifest
4664
5466
  };
4665
5467
  }
4666
5468
  function readCutList(project) {
4667
- if (!(0, import_node_fs12.existsSync)(project.cutsPath)) {
5469
+ if (!(0, import_node_fs13.existsSync)(project.cutsPath)) {
4668
5470
  return { version: VIDEO_CUTLIST_VERSION, source: project.manifest.source, cuts: [] };
4669
5471
  }
4670
- const raw = JSON.parse((0, import_node_fs12.readFileSync)(project.cutsPath, "utf-8"));
5472
+ const raw = JSON.parse((0, import_node_fs13.readFileSync)(project.cutsPath, "utf-8"));
4671
5473
  const cuts = raw.cuts.map((entry) => {
4672
5474
  if ("rawStart" in entry) return entry;
4673
5475
  return {
@@ -4686,11 +5488,11 @@ function readCutList(project) {
4686
5488
  }
4687
5489
  function writeCutList(project, cuts) {
4688
5490
  const payload = { ...cuts, version: VIDEO_CUTLIST_VERSION };
4689
- (0, import_node_fs12.writeFileSync)(project.cutsPath, JSON.stringify(payload, null, 2), "utf-8");
5491
+ (0, import_node_fs13.writeFileSync)(project.cutsPath, JSON.stringify(payload, null, 2), "utf-8");
4690
5492
  }
4691
5493
  function readTranscript(project) {
4692
- if (!(0, import_node_fs12.existsSync)(project.transcriptPath)) return null;
4693
- const raw = JSON.parse((0, import_node_fs12.readFileSync)(project.transcriptPath, "utf-8"));
5494
+ if (!(0, import_node_fs13.existsSync)(project.transcriptPath)) return null;
5495
+ const raw = JSON.parse((0, import_node_fs13.readFileSync)(project.transcriptPath, "utf-8"));
4694
5496
  const segments = raw.segments.map((seg) => ({
4695
5497
  text: seg.text,
4696
5498
  start: seg.start,
@@ -4714,13 +5516,13 @@ function readTranscript(project) {
4714
5516
  }
4715
5517
  function writeTranscript(project, transcript) {
4716
5518
  const payload = { ...transcript, version: VIDEO_TRANSCRIPT_VERSION };
4717
- (0, import_node_fs12.writeFileSync)(project.transcriptPath, JSON.stringify(payload, null, 2), "utf-8");
5519
+ (0, import_node_fs13.writeFileSync)(project.transcriptPath, JSON.stringify(payload, null, 2), "utf-8");
4718
5520
  }
4719
5521
  function readComposition(project) {
4720
- return JSON.parse((0, import_node_fs12.readFileSync)(project.compositionPath, "utf-8"));
5522
+ return JSON.parse((0, import_node_fs13.readFileSync)(project.compositionPath, "utf-8"));
4721
5523
  }
4722
5524
  function writeComposition(project, doc) {
4723
- (0, import_node_fs12.writeFileSync)(project.compositionPath, JSON.stringify(doc, null, 2), "utf-8");
5525
+ (0, import_node_fs13.writeFileSync)(project.compositionPath, JSON.stringify(doc, null, 2), "utf-8");
4724
5526
  }
4725
5527
  function rewriteCutLayers(doc, cuts, sourceFilename, sourceDuration, assetId = "src") {
4726
5528
  const preserved = doc.layers.filter((l) => !(l.tags ?? []).includes("silence-trim"));
@@ -4817,7 +5619,7 @@ function parseSilenceDetectStderr(stderr) {
4817
5619
  return intervals;
4818
5620
  }
4819
5621
  function runCapture(cmd, args) {
4820
- return new Promise((resolve14, reject) => {
5622
+ return new Promise((resolve28, reject) => {
4821
5623
  const proc = (0, import_node_child_process2.spawn)(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
4822
5624
  let stdout = "";
4823
5625
  proc.stdout.on("data", (b) => stdout += b.toString());
@@ -4831,12 +5633,12 @@ function runCapture(cmd, args) {
4831
5633
  });
4832
5634
  proc.on("close", (code) => {
4833
5635
  if (code !== 0) reject(new Error(`${cmd} exited ${code}`));
4834
- else resolve14(stdout);
5636
+ else resolve28(stdout);
4835
5637
  });
4836
5638
  });
4837
5639
  }
4838
5640
  function runCaptureStderr(cmd, args) {
4839
- return new Promise((resolve14, reject) => {
5641
+ return new Promise((resolve28, reject) => {
4840
5642
  const proc = (0, import_node_child_process2.spawn)(cmd, args, { stdio: ["ignore", "ignore", "pipe"] });
4841
5643
  let stderr = "";
4842
5644
  proc.stderr.on("data", (b) => stderr += b.toString());
@@ -4848,7 +5650,7 @@ function runCaptureStderr(cmd, args) {
4848
5650
  reject(err);
4849
5651
  }
4850
5652
  });
4851
- proc.on("close", () => resolve14(stderr));
5653
+ proc.on("close", () => resolve28(stderr));
4852
5654
  });
4853
5655
  }
4854
5656
 
@@ -5053,38 +5855,155 @@ function trimCommand(program) {
5053
5855
 
5054
5856
  // src/lib/whisper.ts
5055
5857
  var import_node_child_process3 = require("child_process");
5056
- var import_node_fs13 = require("fs");
5858
+ var import_node_fs14 = require("fs");
5859
+ var import_node_path14 = require("path");
5860
+ var import_node_os2 = require("os");
5861
+ var import_promises = require("stream/promises");
5862
+ var import_node_stream = require("stream");
5057
5863
  async function probeWhisper() {
5058
5864
  if (await commandExists("whisper-cli")) return "whisper-cpp";
5059
5865
  if (process.env.OPENAI_API_KEY) return "openai-api";
5060
5866
  return "none";
5061
5867
  }
5062
- async function runWhisperCpp(sourcePath, options = {}) {
5063
- const model = options.modelPath ?? options.model ?? "base.en";
5064
- const args = [
5065
- sourcePath,
5066
- "--model",
5067
- model,
5068
- "--output-json",
5069
- "--word-thold",
5070
- "0.01",
5071
- // emit word-level timestamps
5072
- "--print-progress",
5073
- "false"
5074
- ];
5075
- if (options.language) {
5076
- args.push("--language", options.language);
5868
+ function whisperCacheDir() {
5869
+ return (0, import_node_path14.join)((0, import_node_os2.homedir)(), ".cache", "whisper.cpp", "models");
5870
+ }
5871
+ function modelCachePath(model) {
5872
+ return (0, import_node_path14.join)(whisperCacheDir(), `ggml-${model}.bin`);
5873
+ }
5874
+ async function resolveWhisperModelPath(model, onProgress) {
5875
+ if ((0, import_node_path14.isAbsolute)(model)) {
5876
+ if (!(0, import_node_fs14.existsSync)(model)) {
5877
+ throw new Error(`whisper model file not found at absolute path: ${model}`);
5878
+ }
5879
+ return model;
5880
+ }
5881
+ const cached = modelCachePath(model);
5882
+ if ((0, import_node_fs14.existsSync)(cached)) return cached;
5883
+ await downloadWhisperModel(model, onProgress);
5884
+ if (!(0, import_node_fs14.existsSync)(cached)) {
5885
+ throw new Error(
5886
+ `whisper model download appeared to succeed but the file is missing at ${cached}`
5887
+ );
5077
5888
  }
5078
- return runCaptureStdout("whisper-cli", args);
5889
+ return cached;
5079
5890
  }
5080
- function parseWhisperCppJson(jsonStr) {
5081
- const raw = JSON.parse(jsonStr);
5082
- const segments = (raw.transcription ?? []).map((seg) => {
5083
- const segStart = (seg.offsets?.from ?? 0) / 1e3;
5084
- const segEnd = (seg.offsets?.to ?? 0) / 1e3;
5085
- const segText = seg.text.trim();
5086
- let words;
5087
- if (seg.tokens && seg.tokens.length > 0) {
5891
+ async function downloadWhisperModel(model, onProgress) {
5892
+ const dir = whisperCacheDir();
5893
+ if (!(0, import_node_fs14.existsSync)(dir)) (0, import_node_fs14.mkdirSync)(dir, { recursive: true });
5894
+ const dest = modelCachePath(model);
5895
+ const url = `https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-${model}.bin`;
5896
+ const partPath = `${dest}.part`;
5897
+ const res = await fetch(url);
5898
+ if (!res.ok) {
5899
+ throw new Error(
5900
+ `failed to download whisper model "${model}" (HTTP ${res.status} ${res.statusText}) from ${url} \u2014 check the model name`
5901
+ );
5902
+ }
5903
+ if (!res.body) {
5904
+ throw new Error(`whisper model download produced no body (model: ${model})`);
5905
+ }
5906
+ const totalHeader = res.headers.get("content-length");
5907
+ const total = totalHeader ? parseInt(totalHeader, 10) : null;
5908
+ let received = 0;
5909
+ const nodeStream = import_node_stream.Readable.fromWeb(res.body);
5910
+ nodeStream.on("data", (chunk) => {
5911
+ received += chunk.length;
5912
+ if (onProgress) onProgress(received, total);
5913
+ });
5914
+ await (0, import_promises.pipeline)(nodeStream, (0, import_node_fs14.createWriteStream)(partPath));
5915
+ const { renameSync, unlinkSync: unlinkSync2 } = await import("fs");
5916
+ if ((0, import_node_fs14.existsSync)(dest)) {
5917
+ try {
5918
+ unlinkSync2(dest);
5919
+ } catch {
5920
+ }
5921
+ }
5922
+ renameSync(partPath, dest);
5923
+ return dest;
5924
+ }
5925
+ async function runWhisperCpp(sourcePath, options = {}) {
5926
+ const modelName = options.modelPath ?? options.model ?? "base.en";
5927
+ const resolvedModelPath = await resolveWhisperModelPath(modelName, options.onProgress);
5928
+ const { audioPath, tempDir } = await extractAudioForWhisper(sourcePath);
5929
+ try {
5930
+ const args = [
5931
+ audioPath,
5932
+ "--model",
5933
+ resolvedModelPath,
5934
+ "--output-json-full",
5935
+ // word-level timestamps in the JSON output
5936
+ "--word-thold",
5937
+ "0.01",
5938
+ "--print-progress",
5939
+ "false"
5940
+ ];
5941
+ if (options.language) {
5942
+ args.push("--language", options.language);
5943
+ }
5944
+ const stdout = await runCaptureStdout("whisper-cli", args);
5945
+ const jsonSidecar = `${audioPath}.json`;
5946
+ if ((0, import_node_fs14.existsSync)(jsonSidecar)) {
5947
+ return (0, import_node_fs14.readFileSync)(jsonSidecar, "utf-8");
5948
+ }
5949
+ return stdout;
5950
+ } finally {
5951
+ if (tempDir) {
5952
+ try {
5953
+ (0, import_node_fs14.rmSync)(tempDir, { recursive: true, force: true });
5954
+ } catch {
5955
+ }
5956
+ }
5957
+ }
5958
+ }
5959
+ async function extractAudioForWhisper(sourcePath) {
5960
+ const lower = sourcePath.toLowerCase();
5961
+ if (lower.endsWith(".wav")) return { audioPath: sourcePath, tempDir: null };
5962
+ if (!await commandExists("ffmpeg")) {
5963
+ throw new Error(
5964
+ `ffmpeg not found on PATH. Whisper.cpp only ingests WAV; non-WAV sources (${sourcePath}) need ffmpeg to extract audio. Install with: brew install ffmpeg.`
5965
+ );
5966
+ }
5967
+ const tempDir = (0, import_node_fs14.mkdtempSync)((0, import_node_path14.join)((0, import_node_os2.tmpdir)(), "atelier-whisper-"));
5968
+ const audioPath = (0, import_node_path14.join)(tempDir, "audio.wav");
5969
+ await runOk("ffmpeg", [
5970
+ "-loglevel",
5971
+ "error",
5972
+ "-y",
5973
+ "-i",
5974
+ sourcePath,
5975
+ "-ar",
5976
+ "16000",
5977
+ "-ac",
5978
+ "1",
5979
+ "-c:a",
5980
+ "pcm_s16le",
5981
+ audioPath
5982
+ ]);
5983
+ return { audioPath, tempDir };
5984
+ }
5985
+ async function runOk(cmd, args) {
5986
+ return new Promise((resolve28, reject) => {
5987
+ const p = (0, import_node_child_process3.spawn)(cmd, args, { stdio: ["ignore", "ignore", "pipe"] });
5988
+ let stderr = "";
5989
+ p.stderr?.on("data", (chunk) => {
5990
+ stderr += chunk.toString();
5991
+ });
5992
+ p.on("error", reject);
5993
+ p.on("close", (code) => {
5994
+ if (code === 0) resolve28();
5995
+ else reject(new Error(`${cmd} exited ${code}: ${stderr.trim() || "(no stderr)"}`));
5996
+ });
5997
+ });
5998
+ }
5999
+ function parseWhisperCppJson(jsonStr) {
6000
+ const raw = JSON.parse(jsonStr);
6001
+ const segments = (raw.transcription ?? []).map((seg) => {
6002
+ const segStart = (seg.offsets?.from ?? 0) / 1e3;
6003
+ const segEnd = (seg.offsets?.to ?? 0) / 1e3;
6004
+ const segText = seg.text.trim();
6005
+ let words;
6006
+ if (seg.tokens && seg.tokens.length > 0) {
5088
6007
  words = seg.tokens.filter((t) => t.text.trim().length > 0 && !t.text.startsWith("[_")).map((t) => ({
5089
6008
  detected: t.text.trim(),
5090
6009
  text: t.text.trim(),
@@ -5118,18 +6037,18 @@ function parseWhisperCppJson(jsonStr) {
5118
6037
  }
5119
6038
  async function commandExists(name) {
5120
6039
  if (name.startsWith("/") || name.match(/^[A-Z]:\\/)) {
5121
- return (0, import_node_fs13.existsSync)(name);
6040
+ return (0, import_node_fs14.existsSync)(name);
5122
6041
  }
5123
- return new Promise((resolve14) => {
6042
+ return new Promise((resolve28) => {
5124
6043
  const probe = (0, import_node_child_process3.spawn)(process.platform === "win32" ? "where" : "which", [name], {
5125
6044
  stdio: ["ignore", "ignore", "ignore"]
5126
6045
  });
5127
- probe.on("error", () => resolve14(false));
5128
- probe.on("close", (code) => resolve14(code === 0));
6046
+ probe.on("error", () => resolve28(false));
6047
+ probe.on("close", (code) => resolve28(code === 0));
5129
6048
  });
5130
6049
  }
5131
6050
  function runCaptureStdout(cmd, args) {
5132
- return new Promise((resolve14, reject) => {
6051
+ return new Promise((resolve28, reject) => {
5133
6052
  const proc = (0, import_node_child_process3.spawn)(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
5134
6053
  let stdout = "";
5135
6054
  let stderr = "";
@@ -5150,13 +6069,16 @@ function runCaptureStdout(cmd, args) {
5150
6069
  reject(new Error(`${cmd} exited ${code}
5151
6070
  ${stderr}`));
5152
6071
  } else {
5153
- resolve14(stdout);
6072
+ resolve28(stdout);
5154
6073
  }
5155
6074
  });
5156
6075
  });
5157
6076
  }
5158
6077
 
5159
6078
  // src/lib/transcript-model.ts
6079
+ var import_node_crypto = require("crypto");
6080
+ var import_node_fs15 = require("fs");
6081
+ var import_node_path15 = require("path");
5160
6082
  var DEFAULT_TRANSCRIPT_MATCH_TOLERANCE = 0.3;
5161
6083
  var DEFAULT_PHRASE_MAX_WORDS = 5;
5162
6084
  var DEFAULT_PHRASE_PAUSE_GAP_SECONDS = 0.4;
@@ -5452,13 +6374,24 @@ function buildPhraseDeltas(layerId, phrase, fadeSeconds, fps) {
5452
6374
  const fadeFrames = Math.max(1, Math.round(fadeSeconds * fps));
5453
6375
  const startFrame = Math.floor(phrase.start * fps);
5454
6376
  const endFrame = Math.ceil(phrase.end * fps);
5455
- const fadeOutStart = Math.max(startFrame + 1, endFrame - fadeFrames);
6377
+ const phraseFrames = Math.max(1, endFrame - startFrame);
6378
+ const fadeInFrames = Math.min(fadeFrames, Math.floor(phraseFrames / 2));
6379
+ const fadeOutStart = Math.max(startFrame + fadeInFrames, endFrame - fadeFrames);
5456
6380
  return [
5457
- // Fade in to visible at phrase start
6381
+ // Fade in AT phrase start (the [start, start+fadeF] window lives INSIDE
6382
+ // the phrase). Previously this was [start - fadeF, start], which placed
6383
+ // the fade-in BEFORE the phrase began — for touching transcript phrases
6384
+ // (end_N === start_{N+1}, the common case), cap-N's fade-out [end-2, end]
6385
+ // and cap-(N+1)'s fade-in [start-2, start] overlapped on the same frame
6386
+ // window, producing a visible 2-frame cross-fade where both captions
6387
+ // rendered at ~0.68 opacity simultaneously (the "captions stacking"
6388
+ // false-bug report L-2026-05-28-ascend-151000-001). Moving the fade-in
6389
+ // INSIDE the phrase means cap-N fades out [88, 90] → cap-(N+1) fades in
6390
+ // [90, 92], no overlap, captions transition discretely.
5458
6391
  {
5459
6392
  layer: layerId,
5460
6393
  property: "opacity",
5461
- range: [Math.max(0, startFrame - fadeFrames), startFrame],
6394
+ range: [startFrame, startFrame + fadeInFrames],
5462
6395
  from: 0,
5463
6396
  to: 1,
5464
6397
  easing: "ease-out"
@@ -5491,8 +6424,10 @@ async function transcribeProject(projectDir, options = {}) {
5491
6424
  "OpenAI API backend is not yet implemented. Install whisper.cpp for local transcription."
5492
6425
  );
5493
6426
  }
5494
- const rawJson = await runWhisperCpp(project.sourcePath, {
6427
+ const sourcePath = options.sourcePath ?? project.sourcePath;
6428
+ const rawJson = await runWhisperCpp(sourcePath, {
5495
6429
  model: options.model,
6430
+ modelPath: options.modelPath,
5496
6431
  language: options.language
5497
6432
  });
5498
6433
  let transcript = parseWhisperCppJson(rawJson);
@@ -5538,10 +6473,11 @@ function formatResult2(result) {
5538
6473
  function transcribeCommand(program) {
5539
6474
  program.command("transcribe <project>").description(
5540
6475
  "Transcribe source video via Whisper, write transcript.json, and rewrite caption-tagged TextVisual layers in project.atelier. Preserves user transcript edits on re-run."
5541
- ).option("--model <name>", "Whisper model: tiny|base|small|medium|large-v3", "base.en").option("--language <code>", "BCP-47 language hint (omit for autodetect)").option("--reset", "Discard existing user edits; full fresh transcript").option("--no-captions", "Write transcript.json only; skip caption layer generation").option("--recipe <name>", "Apply a Studio Recipe's caption_style + caption_grouping").option("--dry-run", "Print transcript; don't write files").option("--json", "Output result as JSON for piping").action(async (project, opts) => {
6476
+ ).option("--model <name>", "Whisper model: tiny|base|small|medium|large-v3", "base.en").option("--model-path <path>", "Explicit path to a .bin model file (overrides --model)").option("--language <code>", "BCP-47 language hint (omit for autodetect)").option("--reset", "Discard existing user edits; full fresh transcript").option("--no-captions", "Write transcript.json only; skip caption layer generation").option("--recipe <name>", "Apply a Studio Recipe's caption_style + caption_grouping").option("--dry-run", "Print transcript; don't write files").option("--json", "Output result as JSON for piping").action(async (project, opts) => {
5542
6477
  try {
5543
6478
  let transcribeOpts = {
5544
6479
  model: opts.model,
6480
+ modelPath: opts.modelPath,
5545
6481
  language: opts.language,
5546
6482
  reset: opts.reset,
5547
6483
  noCaptions: !opts.captions,
@@ -5719,25 +6655,25 @@ function captionsCommand(program) {
5719
6655
  }
5720
6656
 
5721
6657
  // src/commands/recipe.ts
5722
- var import_node_fs14 = require("fs");
5723
- var import_node_path13 = require("path");
6658
+ var import_node_fs16 = require("fs");
6659
+ var import_node_path16 = require("path");
5724
6660
  function recipeCommand(program) {
5725
6661
  const recipe = program.command("recipe").description("Manage Studio Recipes \u2014 reusable style presets");
5726
6662
  recipe.command("new <name>").description("Scaffold a starter recipe YAML with all current defaults filled in").option("--dir <path>", "Where to write the recipe (default: ./.atelier/recipes/)").action((name, opts) => {
5727
6663
  try {
5728
- const baseDir = opts.dir ?? (0, import_node_path13.join)((0, import_node_path13.resolve)(process.cwd()), ".atelier", "recipes");
5729
- if (!(0, import_node_fs14.existsSync)(baseDir)) {
5730
- (0, import_node_fs14.mkdirSync)(baseDir, { recursive: true });
6664
+ const baseDir = opts.dir ?? (0, import_node_path16.join)((0, import_node_path16.resolve)(process.cwd()), ".atelier", "recipes");
6665
+ if (!(0, import_node_fs16.existsSync)(baseDir)) {
6666
+ (0, import_node_fs16.mkdirSync)(baseDir, { recursive: true });
5731
6667
  }
5732
6668
  const hasKnownExt = /\.(recipe\.yaml|recipe\.json|yaml|yml|json)$/i.test(name);
5733
6669
  const fileName = hasKnownExt ? name : `${name}.recipe.yaml`;
5734
- const outPath = (0, import_node_path13.join)(baseDir, fileName);
5735
- if ((0, import_node_fs14.existsSync)(outPath)) {
6670
+ const outPath = (0, import_node_path16.join)(baseDir, fileName);
6671
+ if ((0, import_node_fs16.existsSync)(outPath)) {
5736
6672
  throw new Error(`Recipe already exists at ${outPath} \u2014 refusing to overwrite.`);
5737
6673
  }
5738
6674
  const recipeName = name.replace(/\.(recipe\.yaml|recipe\.json|yaml|yml|json)$/i, "");
5739
6675
  const yaml = scaffoldRecipeYaml(recipeName);
5740
- (0, import_node_fs14.writeFileSync)(outPath, yaml, "utf-8");
6676
+ (0, import_node_fs16.writeFileSync)(outPath, yaml, "utf-8");
5741
6677
  console.log(`Created ${outPath}`);
5742
6678
  } catch (err) {
5743
6679
  console.error(`atelier recipe new: ${err instanceof Error ? err.message : err}`);
@@ -5824,9 +6760,2018 @@ Done.`);
5824
6760
  }
5825
6761
  });
5826
6762
  }
6763
+
6764
+ // src/commands/init.ts
6765
+ var import_node_child_process4 = require("child_process");
6766
+ var import_node_fs22 = require("fs");
6767
+ var import_node_path22 = require("path");
6768
+ var import_node_readline = require("readline");
6769
+
6770
+ // src/commands/artifacts.ts
6771
+ var import_node_path18 = require("path");
6772
+ var import_node_fs18 = require("fs");
6773
+
6774
+ // src/lib/artifact-project.ts
6775
+ var import_node_fs17 = require("fs");
6776
+ var import_node_path17 = require("path");
6777
+ var ARTIFACT_FILENAMES = {
6778
+ design: "DESIGN.md",
6779
+ script: "SCRIPT.md",
6780
+ storyboard: "STORYBOARD.md"
6781
+ };
6782
+ function loadArtifactsFromProject(projectDir) {
6783
+ const absDir = (0, import_node_path17.resolve)(projectDir);
6784
+ if (!(0, import_node_fs17.existsSync)(absDir)) {
6785
+ throw new Error(`project directory does not exist: ${absDir}`);
6786
+ }
6787
+ const stat = (0, import_node_fs17.statSync)(absDir);
6788
+ if (!stat.isDirectory()) {
6789
+ throw new Error(`project path is not a directory: ${absDir}`);
6790
+ }
6791
+ const result = {
6792
+ projectDir: absDir,
6793
+ missing: [],
6794
+ parseErrors: []
6795
+ };
6796
+ for (const slot of ["design", "script", "storyboard"]) {
6797
+ const filename = ARTIFACT_FILENAMES[slot];
6798
+ const filePath = (0, import_node_path17.join)(absDir, filename);
6799
+ if (!(0, import_node_fs17.existsSync)(filePath)) {
6800
+ result.missing.push(filename);
6801
+ continue;
6802
+ }
6803
+ let raw;
6804
+ try {
6805
+ raw = (0, import_node_fs17.readFileSync)(filePath, "utf-8");
6806
+ } catch (err) {
6807
+ result.parseErrors.push({
6808
+ artifact: slot,
6809
+ file: filePath,
6810
+ message: `read failed: ${err instanceof Error ? err.message : String(err)}`
6811
+ });
6812
+ continue;
6813
+ }
6814
+ try {
6815
+ if (slot === "design") {
6816
+ result.design = parseDesign(raw);
6817
+ } else if (slot === "script") {
6818
+ result.script = parseScript(raw);
6819
+ } else {
6820
+ result.storyboard = parseStoryboard(raw);
6821
+ }
6822
+ } catch (err) {
6823
+ result.parseErrors.push({
6824
+ artifact: slot,
6825
+ file: filePath,
6826
+ message: formatParseError(err)
6827
+ });
6828
+ }
6829
+ }
6830
+ return result;
6831
+ }
6832
+ function formatParseError(err) {
6833
+ if (err instanceof Error) {
6834
+ const issues = err.issues;
6835
+ if (Array.isArray(issues) && issues.length > 0) {
6836
+ const formatted = issues.map((issue) => {
6837
+ const path = issue.path.length > 0 ? issue.path.join(".") : "<root>";
6838
+ return `${path}: ${issue.message}`;
6839
+ }).join("; ");
6840
+ return formatted;
6841
+ }
6842
+ return err.message;
6843
+ }
6844
+ return String(err);
6845
+ }
6846
+
6847
+ // src/lib/artifact-templates.ts
6848
+ var DESIGN_TEMPLATE = `---
6849
+ # DESIGN.md \u2014 brand register for this Project.
6850
+ # Authored by Quill. Replace the TODO values with the real ones, then run
6851
+ # \`atelier artifacts validate <project-dir>\` to check the result.
6852
+
6853
+ audience:
6854
+ # TODO: who is this for? Be specific \u2014 a single sentence's worth of person.
6855
+ primary: TODO describe primary audience
6856
+ # TODO: optional second-order audience (lurkers, decision-makers, etc.)
6857
+ # secondary: TODO secondary audience
6858
+ # TODO: where this ships. Closed-ish list; common values: linkedin, x, instagram, youtube-shorts, tiktok.
6859
+ platform: [TODO-platform]
6860
+
6861
+ voice:
6862
+ # TODO: 3 to 5 voice descriptors. Examples: dry, precise, mischievous, warm, clinical, contrarian.
6863
+ descriptors: [TODO-descriptor-1, TODO-descriptor-2, TODO-descriptor-3]
6864
+ # TODO: optional reference voices (writers, speakers, channels). Anchors the tone.
6865
+ # references: [TODO reference]
6866
+
6867
+ visual_register:
6868
+ palette:
6869
+ # TODO: replace these defaults with your real brand tokens.
6870
+ # Roles are closed: background | surface | primary | accent | text-on-dark | text-on-light.
6871
+ # Hex must be #RGB / #RRGGBB / #RRGGBBAA.
6872
+ - { token: paper, hex: "#F4EFE6", role: background }
6873
+ - { token: ink, hex: "#1A1A1A", role: text-on-light }
6874
+ - { token: accent, hex: "#C9533C", role: accent }
6875
+ typography:
6876
+ # TODO: real font tokens. Usage is closed: display | body | caption | mono.
6877
+ - { token: display, family: "TODO-display-family", weights: [400, 700], usage: display }
6878
+ - { token: body, family: "TODO-body-family", weights: [400, 500], usage: body }
6879
+ motion_register:
6880
+ # tempo: snappy | steady | calm | languid
6881
+ # easing_bias: linear | ease-out | spring
6882
+ # camera_bias: static | drift | snap (optional)
6883
+ tempo: steady
6884
+ easing_bias: ease-out
6885
+ camera_bias: static
6886
+
6887
+ constraints:
6888
+ # TODO: optional max duration in seconds (omit if no hard cap).
6889
+ # max_duration_seconds: 75
6890
+ # TODO: closed aspect-ratio set: 9:16 | 1:1 | 16:9 | 4:5.
6891
+ aspect_ratios: ["9:16"]
6892
+ # TODO: prose rules Iris must honor at composition time. Free-text strings.
6893
+ do_not:
6894
+ - TODO add a do-not rule
6895
+
6896
+ brand_references:
6897
+ logos:
6898
+ # TODO: at least one logo token. Role is closed: primary | mark | wordmark.
6899
+ - { token: mark-primary, role: mark }
6900
+ # TODO: optional social handles.
6901
+ # handles:
6902
+ # - { platform: x, value: "@your-handle" }
6903
+ # TODO: optional page-number convention used in carousel exports.
6904
+ # page_number_convention: "n / N, bottom-right, body font, 60% opacity"
6905
+
6906
+ # variances: # only populate when this Project diverges from an attached recipe.
6907
+ # - { field: visual_register.palette.accent, value: "#FF0000", reason: "campaign override" }
6908
+ ---
6909
+
6910
+ # Voice notes
6911
+
6912
+ <!--
6913
+ Free-form prose. Quill's notes on rhythm, signature phrases, taboos.
6914
+ This body is preserved verbatim \u2014 Iris does not parse it; it's for humans
6915
+ and for the next pass of Quill.
6916
+ -->
6917
+
6918
+ > TODO: a paragraph or two on how the voice should *feel*. Punchy openings?
6919
+ > One idea per beat? Reserve the accent color for the most important word?
6920
+ `;
6921
+ var SCRIPT_TEMPLATE = `---
6922
+ # SCRIPT.md \u2014 narrative for this Project.
6923
+ # Authored by Quill, refined by Lux. Replace TODO copy with the real line.
6924
+ # Beat ids are kebab-case and must match the STORYBOARD beat ids.
6925
+
6926
+ mode: narrated # narrated | carousel | text-only
6927
+ target_duration_s: 60 # creator intent; transcript binding can rebind beat windows post-VO
6928
+ language: en-US
6929
+ # tts_voice: voice.host.primary # optional slot ref; resolved at TTS time
6930
+ ---
6931
+
6932
+ # Hook
6933
+ # At least one beat. The hook earns the rest of the watch.
6934
+ - id: hook-1
6935
+ copy: "TODO: contrarian opener \u2014 one sentence that earns the next beat."
6936
+ intent: "TODO why this opener works (one sentence)."
6937
+ est_duration_s: 3.0
6938
+ bind_to_transcript: true
6939
+
6940
+ # Story
6941
+ # Optional block; remove if not used. Establish receipts.
6942
+ - id: story-1
6943
+ copy: "TODO: who you are / why this is credible \u2014 one sentence."
6944
+ intent: "TODO establish receipts; promise specificity."
6945
+ est_duration_s: 3.0
6946
+ bind_to_transcript: true
6947
+
6948
+ # Proof
6949
+ # Optional block; remove if not used. Concrete claims.
6950
+ - id: proof-1
6951
+ copy: "TODO: the most defensible claim, stated plainly."
6952
+ intent: "TODO lead with what you can defend hardest."
6953
+ est_duration_s: 3.0
6954
+ bind_to_transcript: true
6955
+
6956
+ # CTA
6957
+ # At least one beat. Single token call-to-action; no verb pileup.
6958
+ - id: cta-1
6959
+ copy: "TODO: link in bio. / one short ask."
6960
+ intent: "TODO the single action you want from the viewer."
6961
+ est_duration_s: 2.0
6962
+ bind_to_transcript: true
6963
+ `;
6964
+ var STORYBOARD_TEMPLATE = `---
6965
+ # STORYBOARD.md \u2014 composition beats for this Project.
6966
+ # Authored by Lux. One beat per SCRIPT beat (plus optional b-roll.* beats).
6967
+ # Iris walks this file beat by beat to mutate the live AtelierDocument.
6968
+
6969
+ technique_library_version: 1 # bump on additive technique-library changes
6970
+ aspect_ratio: "9:16" # closed: 9:16 | 1:1 | 16:9 | 4:5
6971
+ ---
6972
+
6973
+ ## hook-1
6974
+ # TODO: matches SCRIPT beat \`hook-1\`. Tune mood / camera / slots to match the line.
6975
+ window: { start_s: 0.0, end_s: 3.0 }
6976
+ mood: [clinical] # adjectives; closed-ish vocab \u2014 tense, warm, clinical, mischievous, ...
6977
+ camera: { kind: static } # kind: static | drift | push | pull | whip
6978
+ slots:
6979
+ # Typed slot refs: <kind>.<category>.<specifier>.
6980
+ # Kind is closed (image | video | audio | text | font | voice | logo).
6981
+ # \`text.super.from-script\` pulls copy from the matching SCRIPT beat.
6982
+ - text.super.from-script
6983
+ techniques:
6984
+ # Must exist in the active technique library (technique_library_version).
6985
+ # v1 set: text.split-reveal, image.ken-burns.slow, super.weight-pulse-on-keyword, background.color-shift-to-token.
6986
+ - text.split-reveal
6987
+ transition_out: { kind: cut } # kind: cut | dip-to-color | whip | crossfade
6988
+ # sfx:
6989
+ # - { slot: audio.sfx.transition.whoosh, gain_db: -6 }
6990
+
6991
+ ## cta-1
6992
+ # TODO: matches SCRIPT beat \`cta-1\`. Final beat \u2014 keep it clean.
6993
+ window: { start_s: 3.0, end_s: 5.0 }
6994
+ mood: [grounded]
6995
+ camera: { kind: static }
6996
+ slots:
6997
+ - text.super.from-script
6998
+ techniques:
6999
+ - text.split-reveal
7000
+ transition_out: { kind: cut }
7001
+
7002
+ # TODO: add \`## story-1\`, \`## proof-1\`, etc. once Quill writes the matching SCRIPT beats.
7003
+ # Storyboard-only b-roll beats are also allowed; prefix the id with \`b-roll.\`:
7004
+ #
7005
+ # ## b-roll.intro-monitor
7006
+ # window: { start_s: 1.0, end_s: 2.0 }
7007
+ # mood: [clinical]
7008
+ # camera: { kind: drift }
7009
+ # slots:
7010
+ # - image.b-roll.code-monitor
7011
+ # techniques:
7012
+ # - image.ken-burns.slow
7013
+ # transition_out: { kind: crossfade, duration_s: 0.2 }
7014
+ `;
7015
+ var ARTIFACT_TEMPLATES = {
7016
+ design: { filename: "DESIGN.md", content: DESIGN_TEMPLATE },
7017
+ script: { filename: "SCRIPT.md", content: SCRIPT_TEMPLATE },
7018
+ storyboard: { filename: "STORYBOARD.md", content: STORYBOARD_TEMPLATE }
7019
+ };
7020
+ var ARTIFACT_SLOTS = ["design", "script", "storyboard"];
7021
+
7022
+ // src/commands/artifacts.ts
7023
+ function artifactsCommand(program) {
7024
+ const artifacts = program.command("artifacts").description("Manage Atelier front-of-pipeline artifacts (DESIGN/SCRIPT/STORYBOARD)");
7025
+ artifacts.command("validate <project-dir>").description(
7026
+ "Validate the DESIGN.md / SCRIPT.md / STORYBOARD.md triplet inside a Project directory. Runs the ^valid-artifact-set gate; absent artifacts are noted, not failed."
7027
+ ).option("--json", "Emit a machine-readable JSON report instead of human-formatted output").action((projectDir, opts) => {
7028
+ let loaded;
7029
+ try {
7030
+ loaded = loadArtifactsFromProject(projectDir);
7031
+ } catch (err) {
7032
+ const msg = err instanceof Error ? err.message : String(err);
7033
+ if (opts.json) {
7034
+ console.log(JSON.stringify({ ok: false, error: msg }, null, 2));
7035
+ } else {
7036
+ console.error(`atelier artifacts validate: ${msg}`);
7037
+ }
7038
+ process.exit(1);
7039
+ }
7040
+ const validation = validateArtifactSet({
7041
+ design: loaded.design,
7042
+ script: loaded.script,
7043
+ storyboard: loaded.storyboard
7044
+ });
7045
+ const hasParseErrors = loaded.parseErrors.length > 0;
7046
+ const ok = !hasParseErrors && validation.ok;
7047
+ if (opts.json) {
7048
+ emitJson(loaded, validation, ok);
7049
+ } else {
7050
+ emitHuman(loaded, validation, ok);
7051
+ }
7052
+ if (!ok) {
7053
+ process.exit(1);
7054
+ }
7055
+ });
7056
+ artifacts.command("scaffold <project-dir>").description(
7057
+ "Write canonical empty DESIGN.md / SCRIPT.md / STORYBOARD.md templates into a Project directory. Creates the directory if absent; refuses to overwrite existing files without --force."
7058
+ ).option(
7059
+ "--only <slots>",
7060
+ "Restrict to a subset (design | script | storyboard); comma-separated or repeated flag.",
7061
+ collectOnly,
7062
+ []
7063
+ ).option("--force", "Overwrite existing artifact files in <project-dir>").option("--json", "Emit a machine-readable JSON report instead of human-formatted output").action((projectDir, opts) => {
7064
+ try {
7065
+ const slots = resolveSlots(opts.only ?? []);
7066
+ const result = scaffoldArtifacts(projectDir, {
7067
+ slots,
7068
+ force: opts.force === true
7069
+ });
7070
+ if (opts.json) {
7071
+ console.log(JSON.stringify({
7072
+ ok: true,
7073
+ projectDir: result.projectDir,
7074
+ created: result.created,
7075
+ skipped: result.skipped
7076
+ }, null, 2));
7077
+ } else {
7078
+ console.log(`Project: ${result.projectDir}`);
7079
+ for (const c of result.created) console.log(` Created ${c}`);
7080
+ for (const s of result.skipped) console.log(` Skipped ${s} (already exists; use --force to overwrite)`);
7081
+ console.log("");
7082
+ console.log(
7083
+ `Result: ${result.created.length} written, ${result.skipped.length} skipped.`
7084
+ );
7085
+ }
7086
+ } catch (err) {
7087
+ const msg = err instanceof Error ? err.message : String(err);
7088
+ if (opts.json) {
7089
+ console.log(JSON.stringify({ ok: false, error: msg }, null, 2));
7090
+ } else {
7091
+ console.error(`atelier artifacts scaffold: ${msg}`);
7092
+ }
7093
+ process.exit(1);
7094
+ }
7095
+ });
7096
+ }
7097
+ function collectOnly(value, previous) {
7098
+ const parts = value.split(",").map((s) => s.trim()).filter((s) => s.length > 0);
7099
+ return [...previous, ...parts];
7100
+ }
7101
+ function resolveSlots(raw) {
7102
+ if (raw.length === 0) return [...ARTIFACT_SLOTS];
7103
+ const seen = /* @__PURE__ */ new Set();
7104
+ for (const token of raw) {
7105
+ if (!isArtifactSlot(token)) {
7106
+ throw new Error(
7107
+ `unknown --only value "${token}" \u2014 expected one of ${ARTIFACT_SLOTS.join(", ")}`
7108
+ );
7109
+ }
7110
+ seen.add(token);
7111
+ }
7112
+ return ARTIFACT_SLOTS.filter((s) => seen.has(s));
7113
+ }
7114
+ function isArtifactSlot(value) {
7115
+ return ARTIFACT_SLOTS.includes(value);
7116
+ }
7117
+ function scaffoldArtifacts(projectDir, opts) {
7118
+ const absDir = (0, import_node_path18.resolve)(projectDir);
7119
+ if ((0, import_node_fs18.existsSync)(absDir)) {
7120
+ const stat = (0, import_node_fs18.statSync)(absDir);
7121
+ if (!stat.isDirectory()) {
7122
+ throw new Error(`project path is not a directory: ${absDir}`);
7123
+ }
7124
+ } else {
7125
+ (0, import_node_fs18.mkdirSync)(absDir, { recursive: true });
7126
+ }
7127
+ const targets = opts.slots.map((slot) => ({
7128
+ slot,
7129
+ filename: ARTIFACT_TEMPLATES[slot].filename,
7130
+ abs: (0, import_node_path18.join)(absDir, ARTIFACT_TEMPLATES[slot].filename),
7131
+ content: ARTIFACT_TEMPLATES[slot].content
7132
+ }));
7133
+ if (!opts.force) {
7134
+ const clobbered = targets.filter((t) => (0, import_node_fs18.existsSync)(t.abs)).map((t) => t.abs);
7135
+ if (clobbered.length > 0) {
7136
+ throw new Error(
7137
+ `refusing to overwrite existing file${clobbered.length === 1 ? "" : "s"} (use --force to replace): ${clobbered.join(", ")}`
7138
+ );
7139
+ }
7140
+ }
7141
+ const created = [];
7142
+ const skipped = [];
7143
+ for (const t of targets) {
7144
+ if (!opts.force && (0, import_node_fs18.existsSync)(t.abs)) {
7145
+ skipped.push(t.abs);
7146
+ continue;
7147
+ }
7148
+ (0, import_node_fs18.writeFileSync)(t.abs, t.content, "utf-8");
7149
+ created.push(t.abs);
7150
+ }
7151
+ return { projectDir: absDir, created, skipped };
7152
+ }
7153
+ function emitJson(loaded, validation, ok) {
7154
+ const payload = {
7155
+ ok,
7156
+ projectDir: loaded.projectDir,
7157
+ present: {
7158
+ design: loaded.design !== void 0,
7159
+ script: loaded.script !== void 0,
7160
+ storyboard: loaded.storyboard !== void 0
7161
+ },
7162
+ missing: loaded.missing,
7163
+ parseErrors: loaded.parseErrors.map((e) => ({
7164
+ artifact: e.artifact,
7165
+ file: e.file,
7166
+ message: e.message
7167
+ })),
7168
+ validation: {
7169
+ ok: validation.ok,
7170
+ warnings: validation.warnings,
7171
+ errors: validation.errors
7172
+ }
7173
+ };
7174
+ console.log(JSON.stringify(payload, null, 2));
7175
+ }
7176
+ function emitHuman(loaded, validation, ok) {
7177
+ console.log(`Project: ${loaded.projectDir}`);
7178
+ for (const slot of ["design", "script", "storyboard"]) {
7179
+ const filename = ARTIFACT_FILENAMES[slot];
7180
+ const present = loaded[slot] !== void 0;
7181
+ const parseErr = loaded.parseErrors.find((e) => e.artifact === slot);
7182
+ if (parseErr) {
7183
+ console.log(` ${filename} FAIL (parse error)`);
7184
+ console.log(` ${formatRelativeFile(parseErr.file, loaded.projectDir)}: ${parseErr.message}`);
7185
+ } else if (present) {
7186
+ console.log(` ${filename} OK`);
7187
+ } else {
7188
+ console.log(` ${filename} [not present]`);
7189
+ }
7190
+ }
7191
+ console.log("");
7192
+ console.log("Cross-artifact validation:");
7193
+ if (validation.errors.length === 0 && validation.warnings.length === 0) {
7194
+ console.log(" PASS (no warnings, no errors)");
7195
+ } else {
7196
+ if (validation.errors.length > 0) {
7197
+ console.log(` Errors (${validation.errors.length}):`);
7198
+ for (const e of validation.errors) {
7199
+ console.log(` - ${e}`);
7200
+ }
7201
+ }
7202
+ if (validation.warnings.length > 0) {
7203
+ console.log(` Warnings (${validation.warnings.length}):`);
7204
+ for (const w of validation.warnings) {
7205
+ console.log(` - ${w}`);
7206
+ }
7207
+ }
7208
+ }
7209
+ console.log("");
7210
+ console.log(ok ? "Result: PASS" : "Result: FAIL");
7211
+ }
7212
+ function formatRelativeFile(file, projectDir) {
7213
+ try {
7214
+ const rel = (0, import_node_path18.relative)(projectDir, file);
7215
+ return rel.length > 0 ? rel : file;
7216
+ } catch {
7217
+ return file;
7218
+ }
7219
+ }
7220
+
7221
+ // src/lib/learning-mode.ts
7222
+ var import_node_fs21 = require("fs");
7223
+ var import_node_path21 = require("path");
7224
+
7225
+ // src/lib/workspace.ts
7226
+ var import_node_fs20 = require("fs");
7227
+ var import_node_path20 = require("path");
7228
+
7229
+ // src/lib/paradigm-augment.ts
7230
+ var import_node_fs19 = require("fs");
7231
+ var import_node_path19 = require("path");
7232
+
7233
+ // src/lib/workspace.ts
7234
+ var WORKSPACE_MANIFEST_FILENAME = "workspace.atelier";
7235
+ function findWorkspace(cwd) {
7236
+ let dir = (0, import_node_path20.resolve)(cwd);
7237
+ while (true) {
7238
+ const candidate = (0, import_node_path20.join)(dir, WORKSPACE_MANIFEST_FILENAME);
7239
+ if ((0, import_node_fs20.existsSync)(candidate)) {
7240
+ const manifest = readWorkspace(dir);
7241
+ return { workspaceDir: dir, manifest };
7242
+ }
7243
+ const parent = (0, import_node_path20.dirname)(dir);
7244
+ if (parent === dir) return null;
7245
+ dir = parent;
7246
+ }
7247
+ }
7248
+ function readWorkspace(workspaceDir) {
7249
+ const absPath = (0, import_node_path20.join)((0, import_node_path20.resolve)(workspaceDir), WORKSPACE_MANIFEST_FILENAME);
7250
+ if (!(0, import_node_fs20.existsSync)(absPath)) {
7251
+ throw new Error(`workspace manifest not found: ${absPath}`);
7252
+ }
7253
+ const raw = (0, import_node_fs20.readFileSync)(absPath, "utf-8");
7254
+ const parsed = parseWorkspaceYaml(raw);
7255
+ const result = WorkspaceManifestSchema.safeParse(parsed);
7256
+ if (!result.success) {
7257
+ const issues = result.error.issues.map((issue) => {
7258
+ const path = issue.path.length > 0 ? issue.path.join(".") : "<root>";
7259
+ return `${path}: ${issue.message}`;
7260
+ }).join("; ");
7261
+ throw new Error(`workspace.atelier at ${absPath} failed validation \u2014 ${issues}`);
7262
+ }
7263
+ return result.data;
7264
+ }
7265
+ function writeWorkspace(workspaceDir, manifest) {
7266
+ const absDir = (0, import_node_path20.resolve)(workspaceDir);
7267
+ if (!(0, import_node_fs20.existsSync)(absDir)) {
7268
+ (0, import_node_fs20.mkdirSync)(absDir, { recursive: true });
7269
+ }
7270
+ const absPath = (0, import_node_path20.join)(absDir, WORKSPACE_MANIFEST_FILENAME);
7271
+ (0, import_node_fs20.writeFileSync)(absPath, renderWorkspaceYaml(manifest), "utf-8");
7272
+ return absPath;
7273
+ }
7274
+ function hasProjectMarker(dir) {
7275
+ const abs = (0, import_node_path20.resolve)(dir);
7276
+ if ((0, import_node_fs20.existsSync)((0, import_node_path20.join)(abs, "project.atelier"))) return true;
7277
+ const dotAtelier = (0, import_node_path20.join)(abs, ".atelier");
7278
+ if (!(0, import_node_fs20.existsSync)(dotAtelier)) return false;
7279
+ try {
7280
+ return (0, import_node_fs20.statSync)(dotAtelier).isDirectory();
7281
+ } catch {
7282
+ return false;
7283
+ }
7284
+ }
7285
+ function listProjects(workspaceDir) {
7286
+ const absDir = (0, import_node_path20.resolve)(workspaceDir);
7287
+ let entries;
7288
+ try {
7289
+ entries = (0, import_node_fs20.readdirSync)(absDir);
7290
+ } catch {
7291
+ return [];
7292
+ }
7293
+ const out = [];
7294
+ for (const entry of entries) {
7295
+ if (entry.startsWith(".")) continue;
7296
+ const full = (0, import_node_path20.join)(absDir, entry);
7297
+ let st;
7298
+ try {
7299
+ st = (0, import_node_fs20.statSync)(full);
7300
+ } catch {
7301
+ continue;
7302
+ }
7303
+ if (!st.isDirectory()) continue;
7304
+ if (hasProjectMarker(full)) {
7305
+ out.push(entry);
7306
+ }
7307
+ }
7308
+ return out.sort();
7309
+ }
7310
+ function renderWorkspaceYaml(manifest) {
7311
+ const lines = [];
7312
+ lines.push(`# Atelier workspace manifest.`);
7313
+ lines.push(`# Created by \`atelier studio --new\`. Edit fields directly when needed.`);
7314
+ lines.push(`# A workspace holds multiple Projects with shared learning mode + mind-map.`);
7315
+ lines.push(`version: '${manifest.version}'`);
7316
+ lines.push(`name: ${yamlScalar(manifest.name)}`);
7317
+ lines.push(`created: '${manifest.created}'`);
7318
+ lines.push(`default_mode: ${manifest.default_mode}`);
7319
+ if (manifest.description !== void 0) {
7320
+ lines.push(`description: ${yamlScalar(manifest.description)}`);
7321
+ }
7322
+ if (manifest.projects && manifest.projects.length > 0) {
7323
+ lines.push(`projects:`);
7324
+ for (const p of manifest.projects) {
7325
+ lines.push(` - ${yamlScalar(p)}`);
7326
+ }
7327
+ } else if (manifest.projects !== void 0) {
7328
+ lines.push(`projects: []`);
7329
+ }
7330
+ return lines.join("\n") + "\n";
7331
+ }
7332
+ function parseWorkspaceYaml(raw) {
7333
+ const fields = {};
7334
+ let listKey = null;
7335
+ let listItems = [];
7336
+ const flushList = () => {
7337
+ if (listKey !== null) {
7338
+ fields[listKey] = listItems;
7339
+ listKey = null;
7340
+ listItems = [];
7341
+ }
7342
+ };
7343
+ for (const rawLine of raw.split(/\r?\n/)) {
7344
+ const lineNoComment = stripTrailingComment(rawLine);
7345
+ if (lineNoComment.trim().length === 0) continue;
7346
+ if (listKey !== null) {
7347
+ const itemMatch = lineNoComment.match(/^\s+-\s+(.+?)\s*$/);
7348
+ if (itemMatch) {
7349
+ listItems.push(stripYamlScalar(itemMatch[1]));
7350
+ continue;
7351
+ }
7352
+ flushList();
7353
+ }
7354
+ const kv = lineNoComment.match(/^([A-Za-z_][\w-]*)\s*:\s*(.*)$/);
7355
+ if (!kv) continue;
7356
+ const key = kv[1];
7357
+ const value = kv[2].trim();
7358
+ if (value.length === 0) {
7359
+ listKey = key;
7360
+ listItems = [];
7361
+ continue;
7362
+ }
7363
+ if (value === "[]") {
7364
+ fields[key] = [];
7365
+ continue;
7366
+ }
7367
+ fields[key] = stripYamlScalar(value);
7368
+ }
7369
+ flushList();
7370
+ return fields;
7371
+ }
7372
+ function stripYamlScalar(value) {
7373
+ if (value.length >= 2) {
7374
+ const first = value[0];
7375
+ const last = value[value.length - 1];
7376
+ if (first === '"' && last === '"' || first === "'" && last === "'") {
7377
+ return value.slice(1, -1);
7378
+ }
7379
+ }
7380
+ return value;
7381
+ }
7382
+ function yamlScalar(value) {
7383
+ if (value.length === 0) return "''";
7384
+ if (/^[\w./\- ]+$/.test(value) && !/^\s/.test(value) && !/\s$/.test(value)) {
7385
+ return value;
7386
+ }
7387
+ return `'${value.replace(/'/g, "''")}'`;
7388
+ }
7389
+ function stripTrailingComment(line) {
7390
+ let inSingle = false;
7391
+ let inDouble = false;
7392
+ for (let i = 0; i < line.length; i++) {
7393
+ const c = line[i];
7394
+ if (c === "'" && !inDouble) inSingle = !inSingle;
7395
+ else if (c === '"' && !inSingle) inDouble = !inDouble;
7396
+ else if (c === "#" && !inSingle && !inDouble) return line.slice(0, i);
7397
+ }
7398
+ return line;
7399
+ }
7400
+
7401
+ // src/lib/learning-mode.ts
7402
+ var LEARNING_MODES = ["ambient", "explicit"];
7403
+ var LEARNING_MODE_VERSION = "1.0";
7404
+ function learningModePath(projectDir) {
7405
+ return (0, import_node_path21.join)((0, import_node_path21.resolve)(projectDir), ".atelier", "learning-mode.yaml");
7406
+ }
7407
+ function writeLearningMode(projectDir, mode, opts = {}) {
7408
+ const absPath = learningModePath(projectDir);
7409
+ const dir = (0, import_node_path21.dirname)(absPath);
7410
+ if (!(0, import_node_fs21.existsSync)(dir)) {
7411
+ (0, import_node_fs21.mkdirSync)(dir, { recursive: true });
7412
+ }
7413
+ const ts = (opts.chosenAt ?? /* @__PURE__ */ new Date()).toISOString();
7414
+ const content = renderLearningModeYaml({ version: LEARNING_MODE_VERSION, mode, chosen_at: ts });
7415
+ (0, import_node_fs21.writeFileSync)(absPath, content, "utf-8");
7416
+ return absPath;
7417
+ }
7418
+ function readLearningMode(projectDir) {
7419
+ const absPath = learningModePath(projectDir);
7420
+ if (!(0, import_node_fs21.existsSync)(absPath)) return void 0;
7421
+ const raw = (0, import_node_fs21.readFileSync)(absPath, "utf-8");
7422
+ const fields = {};
7423
+ for (const line of raw.split(/\r?\n/)) {
7424
+ const trimmed = line.trim();
7425
+ if (trimmed.length === 0 || trimmed.startsWith("#")) continue;
7426
+ const m = trimmed.match(/^([A-Za-z_][\w-]*)\s*:\s*(.+?)\s*$/);
7427
+ if (!m) continue;
7428
+ fields[m[1]] = stripYamlScalar2(m[2]);
7429
+ }
7430
+ const mode = fields.mode;
7431
+ if (mode !== "ambient" && mode !== "explicit") {
7432
+ throw new Error(
7433
+ `learning-mode.yaml at ${absPath} has invalid mode "${mode ?? "<missing>"}" \u2014 expected one of ${LEARNING_MODES.join(", ")}`
7434
+ );
7435
+ }
7436
+ const chosen_at = fields.chosen_at;
7437
+ if (!chosen_at) {
7438
+ throw new Error(`learning-mode.yaml at ${absPath} is missing chosen_at`);
7439
+ }
7440
+ return {
7441
+ version: fields.version ?? LEARNING_MODE_VERSION,
7442
+ mode,
7443
+ chosen_at
7444
+ };
7445
+ }
7446
+ function renderLearningModeYaml(file) {
7447
+ return `# Atelier creator learning mode.
7448
+ # Set by \`atelier init\`. Change anytime by editing this file \u2014 the system
7449
+ # never auto-prompts to switch (TD-2026-05-26-646).
7450
+ version: '${file.version}'
7451
+ mode: ${file.mode}
7452
+ chosen_at: '${file.chosen_at}'
7453
+ `;
7454
+ }
7455
+ function stripYamlScalar2(value) {
7456
+ if (value.length >= 2) {
7457
+ const first = value[0];
7458
+ const last = value[value.length - 1];
7459
+ if (first === '"' && last === '"' || first === "'" && last === "'") {
7460
+ return value.slice(1, -1);
7461
+ }
7462
+ }
7463
+ return value;
7464
+ }
7465
+
7466
+ // src/commands/init.ts
7467
+ function initCommand(program) {
7468
+ program.command("init <project-dir>").description(
7469
+ "First-run setup for a creator Project: pick a learning mode, scaffold artifacts, create the local mind-map, optionally git-init."
7470
+ ).option(
7471
+ "--mode <ambient|explicit>",
7472
+ "Skip the interactive prompt and set learning mode directly."
7473
+ ).option("--no-git", "Skip `git init` (and skip writing .gitignore).").option(
7474
+ "--no-scaffold",
7475
+ "Skip writing DESIGN.md / SCRIPT.md / STORYBOARD.md templates."
7476
+ ).option(
7477
+ "--force",
7478
+ "Force-overwrite existing artifact files when scaffolding (delegates to `atelier artifacts scaffold --force`)."
7479
+ ).option("--json", "Emit a machine-readable JSON report instead of human-formatted output.").option(
7480
+ "--no-workspace-relative",
7481
+ "When inside a workspace, resolve <project-dir> relative to cwd instead of the workspace root. Useful for power-user paths that escape the workspace."
7482
+ ).action(async (projectDir, opts) => {
7483
+ try {
7484
+ let resolvedProjectDir = projectDir;
7485
+ let inheritedMode;
7486
+ let workspaceCtx = null;
7487
+ const cwd = process.cwd();
7488
+ const workspaceRelative = opts.workspaceRelative !== false;
7489
+ const ws = findWorkspace(cwd);
7490
+ if (ws) {
7491
+ workspaceCtx = ws;
7492
+ if (workspaceRelative && !projectDir.startsWith("/") && !projectDir.startsWith(".")) {
7493
+ resolvedProjectDir = (0, import_node_path22.join)(ws.workspaceDir, projectDir);
7494
+ }
7495
+ if (opts.mode === void 0) {
7496
+ inheritedMode = ws.manifest.default_mode;
7497
+ }
7498
+ }
7499
+ const result = await runInit({
7500
+ projectDir: resolvedProjectDir,
7501
+ mode: opts.mode ?? inheritedMode,
7502
+ modeInheritedFromWorkspace: opts.mode === void 0 && inheritedMode !== void 0,
7503
+ git: opts.git !== false,
7504
+ scaffold: opts.scaffold !== false,
7505
+ force: opts.force === true,
7506
+ json: opts.json === true
7507
+ });
7508
+ if (workspaceCtx) {
7509
+ try {
7510
+ const projects = listProjects(workspaceCtx.workspaceDir);
7511
+ writeWorkspace(workspaceCtx.workspaceDir, {
7512
+ ...workspaceCtx.manifest,
7513
+ version: "1.0",
7514
+ created: workspaceCtx.manifest.created ?? (/* @__PURE__ */ new Date()).toISOString(),
7515
+ projects
7516
+ });
7517
+ } catch {
7518
+ }
7519
+ }
7520
+ if (opts.json) {
7521
+ console.log(JSON.stringify(result, null, 2));
7522
+ } else {
7523
+ for (const line of formatHumanReport(result)) {
7524
+ console.log(line);
7525
+ }
7526
+ }
7527
+ } catch (err) {
7528
+ const msg = err instanceof Error ? err.message : String(err);
7529
+ if (opts.json) {
7530
+ console.log(JSON.stringify({ ok: false, error: msg }, null, 2));
7531
+ } else {
7532
+ console.error(`atelier init: ${msg}`);
7533
+ }
7534
+ process.exit(1);
7535
+ }
7536
+ });
7537
+ }
7538
+ var MIND_MAP_README = `# Your Mind-Map
7539
+
7540
+ This directory holds the creator's local asset library \u2014 clips, memes,
7541
+ graphics, audio \u2014 that the agent team queries when binding typed slots
7542
+ in recipes and storyboards (TD-2026-05-26-229).
7543
+
7544
+ Organize however you want; the \`atelier_mind_map_query\` tool walks
7545
+ recursively and matches by file extension against slot \`kind\` values
7546
+ (image, video, audio, font, etc.).
7547
+
7548
+ Nothing in this directory leaves your machine (TD-2026-05-26-271).
7549
+ `;
7550
+ var DEFAULT_GITIGNORE = `node_modules/
7551
+ *.log
7552
+ .atelier/cache/
7553
+ `;
7554
+ async function runInit(opts) {
7555
+ const absDir = (0, import_node_path22.resolve)(opts.projectDir);
7556
+ const notes = [];
7557
+ let mode;
7558
+ if (opts.mode !== void 0) {
7559
+ const normalized = opts.mode.toLowerCase();
7560
+ if (normalized !== "ambient" && normalized !== "explicit") {
7561
+ throw new Error(
7562
+ `invalid --mode "${opts.mode}" \u2014 expected one of ${LEARNING_MODES.join(", ")}`
7563
+ );
7564
+ }
7565
+ mode = normalized;
7566
+ }
7567
+ if ((0, import_node_fs22.existsSync)(absDir)) {
7568
+ const st = (0, import_node_fs22.statSync)(absDir);
7569
+ if (!st.isDirectory()) {
7570
+ throw new Error(`project path is not a directory: ${absDir}`);
7571
+ }
7572
+ if ((0, import_node_fs22.readdirSync)(absDir).length > 0) {
7573
+ notes.push(`project dir exists and is non-empty; completing init in place`);
7574
+ }
7575
+ } else {
7576
+ (0, import_node_fs22.mkdirSync)(absDir, { recursive: true });
7577
+ }
7578
+ if (mode === void 0) {
7579
+ if (opts.promptForMode) {
7580
+ mode = await opts.promptForMode();
7581
+ } else if (opts.json) {
7582
+ throw new Error(
7583
+ `--mode is required with --json (scripted use must pre-declare; pass --mode ambient or --mode explicit)`
7584
+ );
7585
+ } else {
7586
+ mode = await promptForLearningMode();
7587
+ }
7588
+ }
7589
+ const learningModeFile = writeLearningMode(absDir, mode);
7590
+ if (opts.modeInheritedFromWorkspace) {
7591
+ notes.push(`learning mode inherited from workspace (${mode})`);
7592
+ }
7593
+ const mindMapDir = (0, import_node_path22.join)(absDir, ".atelier", "mind-map");
7594
+ if (!(0, import_node_fs22.existsSync)(mindMapDir)) {
7595
+ (0, import_node_fs22.mkdirSync)(mindMapDir, { recursive: true });
7596
+ }
7597
+ const readmePath = (0, import_node_path22.join)(mindMapDir, "README.md");
7598
+ if (!(0, import_node_fs22.existsSync)(readmePath)) {
7599
+ (0, import_node_fs22.writeFileSync)(readmePath, MIND_MAP_README, "utf-8");
7600
+ }
7601
+ if (opts.writeManifest !== false) {
7602
+ const projectAtelierPath = (0, import_node_path22.join)(absDir, "project.atelier");
7603
+ if (!(0, import_node_fs22.existsSync)(projectAtelierPath)) {
7604
+ const minimalDoc = {
7605
+ version: "1.0",
7606
+ // Default to a vertical short-form canvas (TikTok/Reels/Shorts) at
7607
+ // 30fps; Iris/Lux rewrite canvas dimensions when DESIGN.md specifies
7608
+ // a different aspect ratio.
7609
+ name: (0, import_node_path22.basename)(absDir),
7610
+ canvas: { width: 1080, height: 1920, fps: 30 },
7611
+ layers: [],
7612
+ states: {}
7613
+ };
7614
+ (0, import_node_fs22.writeFileSync)(
7615
+ projectAtelierPath,
7616
+ JSON.stringify(minimalDoc, null, 2) + "\n",
7617
+ "utf-8"
7618
+ );
7619
+ }
7620
+ }
7621
+ const scaffolded = [];
7622
+ const scaffoldSkipped = [];
7623
+ if (opts.scaffold) {
7624
+ try {
7625
+ const sr = scaffoldArtifacts(absDir, {
7626
+ slots: [...ARTIFACT_SLOTS],
7627
+ force: opts.force
7628
+ });
7629
+ scaffolded.push(...sr.created);
7630
+ scaffoldSkipped.push(...sr.skipped);
7631
+ } catch (err) {
7632
+ const msg = err instanceof Error ? err.message : String(err);
7633
+ if (msg.startsWith("refusing to overwrite")) {
7634
+ notes.push(
7635
+ `artifacts already present; skipped scaffold (use --force to replace)`
7636
+ );
7637
+ } else {
7638
+ throw err;
7639
+ }
7640
+ }
7641
+ }
7642
+ let gitInitialized = false;
7643
+ let alreadyInRepo = false;
7644
+ if (opts.git) {
7645
+ if (isInsideGitRepo(absDir)) {
7646
+ alreadyInRepo = true;
7647
+ notes.push(`already inside a git repo; skipped \`git init\``);
7648
+ } else {
7649
+ try {
7650
+ (0, import_node_child_process4.execFileSync)("git", ["init"], { cwd: absDir, stdio: "ignore" });
7651
+ gitInitialized = true;
7652
+ } catch (err) {
7653
+ const msg = err instanceof Error ? err.message : String(err);
7654
+ notes.push(`\`git init\` failed (${msg}); continuing without git`);
7655
+ }
7656
+ const gitignorePath = (0, import_node_path22.join)(absDir, ".gitignore");
7657
+ if (!(0, import_node_fs22.existsSync)(gitignorePath)) {
7658
+ (0, import_node_fs22.writeFileSync)(gitignorePath, DEFAULT_GITIGNORE, "utf-8");
7659
+ }
7660
+ }
7661
+ }
7662
+ return {
7663
+ ok: true,
7664
+ projectDir: absDir,
7665
+ mode,
7666
+ learningModeFile,
7667
+ scaffolded,
7668
+ scaffoldSkipped,
7669
+ mindMapDir,
7670
+ gitInitialized,
7671
+ alreadyInRepo,
7672
+ notes
7673
+ };
7674
+ }
7675
+ function isInsideGitRepo(dir) {
7676
+ try {
7677
+ (0, import_node_child_process4.execFileSync)("git", ["-C", dir, "rev-parse", "--show-toplevel"], {
7678
+ stdio: "ignore"
7679
+ });
7680
+ return true;
7681
+ } catch {
7682
+ return false;
7683
+ }
7684
+ }
7685
+ async function promptForLearningMode() {
7686
+ const rl = (0, import_node_readline.createInterface)({ input: process.stdin, output: process.stdout });
7687
+ try {
7688
+ process.stdout.write(
7689
+ `How should your agent team learn?
7690
+
7691
+ 1) Ambient \u2014 the team captures corrections silently and surfaces patterns when confident.
7692
+ 2) Explicit \u2014 nothing locks in without your "yes"; high-control, more friction.
7693
+
7694
+ `
7695
+ );
7696
+ for (let attempt = 0; attempt < 3; attempt++) {
7697
+ const answer = (await question(rl, `Choose [1/2]: `)).trim().toLowerCase();
7698
+ const parsed = parseModeAnswer(answer);
7699
+ if (parsed) return parsed;
7700
+ process.stdout.write(`Please enter 1, 2, ambient, or explicit.
7701
+ `);
7702
+ }
7703
+ throw new Error(
7704
+ `couldn't determine learning mode after 3 attempts (hint: re-run with --mode ambient or --mode explicit for scripted use)`
7705
+ );
7706
+ } finally {
7707
+ rl.close();
7708
+ }
7709
+ }
7710
+ function parseModeAnswer(answer) {
7711
+ const a = answer.trim().toLowerCase();
7712
+ if (a === "1" || a === "ambient") return "ambient";
7713
+ if (a === "2" || a === "explicit") return "explicit";
7714
+ return void 0;
7715
+ }
7716
+ function question(rl, prompt) {
7717
+ return new Promise((resolvePromise) => {
7718
+ rl.question(prompt, (answer) => resolvePromise(answer));
7719
+ });
7720
+ }
7721
+ function formatHumanReport(r) {
7722
+ const lines = [];
7723
+ lines.push(`Project: ${r.projectDir}`);
7724
+ lines.push(` Learning mode: ${r.mode}`);
7725
+ lines.push(` Wrote ${r.learningModeFile}`);
7726
+ lines.push(` Mind-map: ${r.mindMapDir}`);
7727
+ if (r.scaffolded.length > 0) {
7728
+ lines.push(` Scaffolded artifacts:`);
7729
+ for (const f of r.scaffolded) lines.push(` - ${f}`);
7730
+ }
7731
+ if (r.scaffoldSkipped.length > 0) {
7732
+ lines.push(` Skipped (already exists):`);
7733
+ for (const f of r.scaffoldSkipped) lines.push(` - ${f}`);
7734
+ }
7735
+ if (r.gitInitialized) {
7736
+ lines.push(` Initialized git repo`);
7737
+ } else if (r.alreadyInRepo) {
7738
+ lines.push(` Git: already inside a repo (skipped)`);
7739
+ }
7740
+ for (const note of r.notes) {
7741
+ lines.push(` Note: ${note}`);
7742
+ }
7743
+ lines.push("");
7744
+ lines.push(`Result: PASS`);
7745
+ return lines;
7746
+ }
7747
+
7748
+ // src/lib/compose-video-project.ts
7749
+ var import_node_fs24 = require("fs");
7750
+ var import_node_path24 = require("path");
7751
+
7752
+ // src/lib/transcribe-orchestrator.ts
7753
+ var import_node_fs23 = require("fs");
7754
+ var import_node_path23 = require("path");
7755
+ async function transcribeMediaFile(projectDir, mediaBasename, options = {}) {
7756
+ const absProject = (0, import_node_path23.resolve)(projectDir);
7757
+ const stem = stripExt((0, import_node_path23.basename)(mediaBasename));
7758
+ if (!stem) {
7759
+ throw new Error(
7760
+ `transcribeMediaFile: invalid mediaBasename "${mediaBasename}" \u2014 derived stem is empty`
7761
+ );
7762
+ }
7763
+ const { transcribeFn, ...transcribeOpts } = options;
7764
+ const fn = transcribeFn ?? transcribeProject;
7765
+ const rootTranscript = (0, import_node_path23.join)(absProject, "transcript.json");
7766
+ const priorRoot = (0, import_node_fs23.existsSync)(rootTranscript) ? (0, import_node_fs23.readFileSync)(rootTranscript) : null;
7767
+ const result = await fn(absProject, transcribeOpts);
7768
+ if (transcribeOpts.dryRun) {
7769
+ return {
7770
+ ...result,
7771
+ mirroredTranscriptPath: (0, import_node_path23.join)(absProject, "transcripts", `${stem}.json`),
7772
+ mediaStem: stem
7773
+ };
7774
+ }
7775
+ const transcriptsDir = (0, import_node_path23.join)(absProject, "transcripts");
7776
+ if (!(0, import_node_fs23.existsSync)(transcriptsDir)) {
7777
+ (0, import_node_fs23.mkdirSync)(transcriptsDir, { recursive: true });
7778
+ }
7779
+ const mirroredTranscriptPath = (0, import_node_path23.join)(transcriptsDir, `${stem}.json`);
7780
+ if ((0, import_node_fs23.existsSync)(rootTranscript)) {
7781
+ (0, import_node_fs23.copyFileSync)(rootTranscript, mirroredTranscriptPath);
7782
+ }
7783
+ if (priorRoot !== null) {
7784
+ (0, import_node_fs23.writeFileSync)(rootTranscript, priorRoot);
7785
+ } else {
7786
+ try {
7787
+ (0, import_node_fs23.rmSync)(rootTranscript);
7788
+ } catch {
7789
+ }
7790
+ }
7791
+ return {
7792
+ ...result,
7793
+ mirroredTranscriptPath,
7794
+ mediaStem: stem
7795
+ };
7796
+ }
7797
+ function stripExt(name) {
7798
+ const ext = (0, import_node_path23.extname)(name);
7799
+ if (!ext) return name;
7800
+ return name.slice(0, -ext.length);
7801
+ }
7802
+
7803
+ // src/lib/timeline-ops.ts
7804
+ var VIDEO_TRACK_ID = "track-video-1";
7805
+ function endOfTrackFrame(layers, trackId, fps) {
7806
+ let maxEnd = 0;
7807
+ for (const layer of layers) {
7808
+ if (layer.parentId !== trackId) continue;
7809
+ const v = layer.visual;
7810
+ if (v.type !== "video" && v.type !== "audio") continue;
7811
+ const tv = v;
7812
+ const start = tv.startFrame ?? 0;
7813
+ const offset = tv.sourceOffset ?? 0;
7814
+ const end = tv.sourceEnd;
7815
+ if (end === void 0) continue;
7816
+ const durFrames = Math.max(0, Math.round((end - offset) * fps));
7817
+ const layerEnd = start + durFrames;
7818
+ if (layerEnd > maxEnd) maxEnd = layerEnd;
7819
+ }
7820
+ return maxEnd;
7821
+ }
7822
+ function ensureTrackLayer(doc, trackId, kind) {
7823
+ const exists = doc.layers.some((l) => l.id === trackId);
7824
+ if (exists) return { doc, created: false };
7825
+ const trackLayer = {
7826
+ id: trackId,
7827
+ tags: ["track", kind],
7828
+ description: kind === "video" ? "Video track 1" : "Audio track 1",
7829
+ visual: { type: "group" },
7830
+ frame: { x: 0, y: 0 },
7831
+ bounds: { width: doc.canvas.width, height: doc.canvas.height }
7832
+ };
7833
+ return {
7834
+ doc: { ...doc, layers: [trackLayer, ...doc.layers] },
7835
+ created: true
7836
+ };
7837
+ }
7838
+ function ensureClipLayer(doc, args) {
7839
+ const existing = doc.layers.find((l) => l.id === args.clipLayerId);
7840
+ if (existing) {
7841
+ return { doc, addedLayer: false, clipLayerId: args.clipLayerId };
7842
+ }
7843
+ const ensured = ensureTrackLayer(doc, args.trackId, args.kind);
7844
+ const working = ensured.doc;
7845
+ const startFrame = args.startFrame !== void 0 ? args.startFrame : endOfTrackFrame(working.layers, args.trackId, working.canvas.fps);
7846
+ const sourceOffset = args.sourceOffset ?? 0;
7847
+ const sourceEnd = args.sourceEnd ?? args.clipDurationSeconds;
7848
+ const newAsset = {
7849
+ type: args.kind === "video" ? "video" : "audio",
7850
+ src: args.sourceName,
7851
+ description: `Source: ${args.sourceName}`
7852
+ };
7853
+ const assets = {
7854
+ ...working.assets ?? {},
7855
+ [args.clipLayerId]: newAsset
7856
+ };
7857
+ let newClip;
7858
+ if (args.kind === "video") {
7859
+ const visual = {
7860
+ type: "video",
7861
+ assetId: args.clipLayerId,
7862
+ src: args.sourceName,
7863
+ startFrame,
7864
+ sourceOffset,
7865
+ sourceEnd,
7866
+ playbackRate: 1,
7867
+ objectFit: "contain"
7868
+ };
7869
+ newClip = {
7870
+ id: args.clipLayerId,
7871
+ parentId: args.trackId,
7872
+ visual,
7873
+ frame: { x: 0, y: 0 },
7874
+ bounds: { width: working.canvas.width, height: working.canvas.height }
7875
+ };
7876
+ } else {
7877
+ const visual = {
7878
+ type: "audio",
7879
+ assetId: args.clipLayerId,
7880
+ src: args.sourceName,
7881
+ startFrame,
7882
+ sourceOffset,
7883
+ sourceEnd
7884
+ };
7885
+ newClip = {
7886
+ id: args.clipLayerId,
7887
+ parentId: args.trackId,
7888
+ visual,
7889
+ frame: { x: 0, y: 0 },
7890
+ bounds: { width: 0, height: 0 }
7891
+ };
7892
+ }
7893
+ return {
7894
+ doc: {
7895
+ ...working,
7896
+ assets,
7897
+ layers: [...working.layers, newClip]
7898
+ },
7899
+ addedLayer: true,
7900
+ clipLayerId: args.clipLayerId
7901
+ };
7902
+ }
7903
+ function recomputeTimeline(doc) {
7904
+ const fps = doc.canvas.fps;
7905
+ const trackGroups = doc.layers.filter(
7906
+ (l) => l.visual.type === "group" && (l.tags ?? []).includes("track")
7907
+ );
7908
+ if (trackGroups.length === 0) {
7909
+ if (doc.timeline === void 0) return doc;
7910
+ const { timeline: _omit, ...rest } = doc;
7911
+ void _omit;
7912
+ return rest;
7913
+ }
7914
+ const tracks = [];
7915
+ let totalFrames = 0;
7916
+ for (const track of trackGroups) {
7917
+ const tags = track.tags ?? [];
7918
+ const kind = tags.includes("audio") ? "audio" : "video";
7919
+ const children = doc.layers.filter(
7920
+ (l) => l.parentId === track.id && l.visual.type === kind
7921
+ );
7922
+ const clips = children.map((layer) => {
7923
+ const v = layer.visual;
7924
+ const start = v.startFrame ?? 0;
7925
+ const offset = v.sourceOffset ?? 0;
7926
+ const end = v.sourceEnd ?? offset;
7927
+ const durFrames = Math.max(0, Math.round((end - offset) * fps));
7928
+ return {
7929
+ layerId: layer.id,
7930
+ startFrame: start,
7931
+ endFrame: start + durFrames,
7932
+ source: v.src ?? ""
7933
+ };
7934
+ }).sort((a, b) => a.startFrame - b.startFrame);
7935
+ const trackEnd = clips.reduce((m, c) => Math.max(m, c.endFrame), 0);
7936
+ if (trackEnd > totalFrames) totalFrames = trackEnd;
7937
+ tracks.push({ id: track.id, kind, clips });
7938
+ }
7939
+ const timeline = { fps, totalFrames, tracks };
7940
+ return { ...doc, timeline };
7941
+ }
7942
+ function stripStaleCaptionArtifacts(doc, clipLayerId) {
7943
+ const droppedIds = /* @__PURE__ */ new Set();
7944
+ const keptLayers = doc.layers.filter((layer) => {
7945
+ if (!(layer.tags ?? []).includes("caption")) return true;
7946
+ if (!layer.parentId) {
7947
+ droppedIds.add(layer.id);
7948
+ return false;
7949
+ }
7950
+ if (layer.parentId === clipLayerId) {
7951
+ droppedIds.add(layer.id);
7952
+ return false;
7953
+ }
7954
+ return true;
7955
+ });
7956
+ if (droppedIds.size === 0) return doc;
7957
+ const states = {};
7958
+ for (const [name, st] of Object.entries(doc.states)) {
7959
+ states[name] = {
7960
+ ...st,
7961
+ deltas: st.deltas.filter((d) => !droppedIds.has(d.layer))
7962
+ };
7963
+ }
7964
+ return { ...doc, layers: keptLayers, states };
7965
+ }
7966
+ function rebuildClipCaptions(doc, clipLayerId, transcript, canvas, captionOptions) {
7967
+ const clipSlug = clipLayerId.startsWith("clip-") ? clipLayerId.slice("clip-".length) : clipLayerId;
7968
+ let working = stripCaptionsForClip(doc, clipLayerId);
7969
+ const built = buildCaptionLayers(transcript, canvas, captionOptions ?? {});
7970
+ const idMap = /* @__PURE__ */ new Map();
7971
+ const renamedLayers = built.layers.map((layer, idx) => {
7972
+ const newId = `cap-${clipSlug}-${idx}`;
7973
+ idMap.set(layer.id, newId);
7974
+ return { ...layer, id: newId, parentId: clipLayerId };
7975
+ });
7976
+ const renamedDeltas = built.deltas.map((d) => ({
7977
+ ...d,
7978
+ layer: idMap.get(d.layer) ?? d.layer
7979
+ }));
7980
+ working = { ...working, layers: [...working.layers, ...renamedLayers] };
7981
+ const stateNames = Object.keys(working.states);
7982
+ const targetStateName = stateNames.includes("default") ? "default" : stateNames[0] ?? "default";
7983
+ const states = { ...working.states };
7984
+ const existing = states[targetStateName];
7985
+ if (existing) {
7986
+ states[targetStateName] = {
7987
+ ...existing,
7988
+ deltas: [...existing.deltas, ...renamedDeltas]
7989
+ };
7990
+ } else {
7991
+ states[targetStateName] = { duration: 0, deltas: renamedDeltas };
7992
+ }
7993
+ return { ...working, states };
7994
+ }
7995
+ function stripCaptionsForClip(doc, clipLayerId) {
7996
+ const droppedIds = /* @__PURE__ */ new Set();
7997
+ const keptLayers = doc.layers.filter((layer) => {
7998
+ if (!(layer.tags ?? []).includes("caption")) return true;
7999
+ if (layer.parentId === clipLayerId) {
8000
+ droppedIds.add(layer.id);
8001
+ return false;
8002
+ }
8003
+ return true;
8004
+ });
8005
+ if (droppedIds.size === 0) return doc;
8006
+ const states = {};
8007
+ for (const [name, st] of Object.entries(doc.states)) {
8008
+ states[name] = {
8009
+ ...st,
8010
+ deltas: st.deltas.filter((d) => !droppedIds.has(d.layer))
8011
+ };
8012
+ }
8013
+ return { ...doc, layers: keptLayers, states };
8014
+ }
8015
+
8016
+ // src/lib/compose-video-project.ts
8017
+ function readAtelierDoc(absPath) {
8018
+ const raw = (0, import_node_fs24.readFileSync)(absPath, "utf-8");
8019
+ const parsed = parseAtelier(raw);
8020
+ if (parsed.success) return parsed.data;
8021
+ return JSON.parse(raw);
8022
+ }
8023
+ var POSSIBLE_SOURCE_EXTS = [".mp4", ".mov", ".webm", ".mkv", ".avi"];
8024
+ function slugifyBasename(name) {
8025
+ const base = (0, import_node_path24.basename)(name);
8026
+ const stem = stripExt2(base);
8027
+ const lower = stem.toLowerCase();
8028
+ const replaced = lower.replace(/[^a-z0-9]+/g, "-");
8029
+ return replaced.replace(/^-+|-+$/g, "");
8030
+ }
8031
+ function stripExt2(name) {
8032
+ const ext = (0, import_node_path24.extname)(name);
8033
+ return ext ? name.slice(0, -ext.length) : name;
8034
+ }
8035
+ async function composeVideoProject(opts) {
8036
+ const projectDir = (0, import_node_path24.resolve)(opts.projectDir);
8037
+ const compositionPath = (0, import_node_path24.join)(projectDir, opts.docPath);
8038
+ (0, import_node_fs24.mkdirSync)((0, import_node_path24.dirname)(compositionPath), { recursive: true });
8039
+ if (!(0, import_node_fs24.existsSync)(compositionPath)) {
8040
+ const seedDoc = {
8041
+ version: "1.0",
8042
+ name: (0, import_node_path24.basename)(projectDir),
8043
+ kind: "video",
8044
+ canvas: { width: 1080, height: 1920, fps: 30 },
8045
+ layers: [],
8046
+ states: { default: { duration: 0, deltas: [] } }
8047
+ };
8048
+ (0, import_node_fs24.writeFileSync)(compositionPath, JSON.stringify(seedDoc, null, 2), "utf-8");
8049
+ }
8050
+ const sourceBasename = canonicalSourceName(opts.sourceFile, projectDir);
8051
+ const clipSlug = slugifyBasename(sourceBasename);
8052
+ if (!clipSlug) {
8053
+ throw new Error(
8054
+ `composeVideoProject: cannot derive a slug from source "${opts.sourceFile}"`
8055
+ );
8056
+ }
8057
+ const clipLayerId = `clip-${clipSlug}`;
8058
+ let transcribeResult;
8059
+ let mirroredTranscriptPath;
8060
+ const shouldTranscribe = opts.transcribe !== false;
8061
+ if (shouldTranscribe) {
8062
+ const noCaptionsOverride = { noCaptions: true };
8063
+ if (opts.mediaBasename) {
8064
+ const result = await transcribeMediaFile(projectDir, opts.mediaBasename, {
8065
+ ...opts.transcribeOptions ?? {},
8066
+ ...noCaptionsOverride,
8067
+ ...opts.transcribeFn !== void 0 ? { transcribeFn: opts.transcribeFn } : {}
8068
+ });
8069
+ transcribeResult = result;
8070
+ mirroredTranscriptPath = result.mirroredTranscriptPath;
8071
+ } else {
8072
+ const fn = opts.transcribeFn ?? transcribeProject;
8073
+ transcribeResult = await fn(projectDir, {
8074
+ ...opts.transcribeOptions ?? {},
8075
+ ...noCaptionsOverride
8076
+ });
8077
+ }
8078
+ }
8079
+ let doc = readAtelierDoc(compositionPath);
8080
+ doc = stripStaleCaptionArtifacts(doc, clipLayerId);
8081
+ const probeFn = opts.probeDurationFn ?? probeDuration;
8082
+ let clipDurationSeconds;
8083
+ try {
8084
+ clipDurationSeconds = await probeFn(opts.sourceFile);
8085
+ } catch {
8086
+ clipDurationSeconds = lastTranscriptWordEnd(transcribeResult?.transcript) ?? 0;
8087
+ }
8088
+ const fps = doc.canvas.fps;
8089
+ const clipDurationFrames = Math.max(1, Math.round(clipDurationSeconds * fps));
8090
+ const { doc: docWithClip, addedLayer } = ensureClipLayer(doc, {
8091
+ clipLayerId,
8092
+ trackId: VIDEO_TRACK_ID,
8093
+ sourceName: sourceBasename,
8094
+ clipDurationSeconds,
8095
+ clipDurationFrames,
8096
+ kind: "video"
8097
+ });
8098
+ doc = docWithClip;
8099
+ if (transcribeResult?.transcript) {
8100
+ doc = rebuildClipCaptions(
8101
+ doc,
8102
+ clipLayerId,
8103
+ transcribeResult.transcript,
8104
+ doc.canvas,
8105
+ opts.captionOptions ?? {}
8106
+ );
8107
+ }
8108
+ doc = recomputeTimeline(doc);
8109
+ const stateNames = Object.keys(doc.states);
8110
+ const targetStateName = stateNames.includes("default") ? "default" : stateNames[0] ?? "default";
8111
+ const totalFrames = doc.timeline?.totalFrames ?? 0;
8112
+ const states = { ...doc.states };
8113
+ const existing = states[targetStateName];
8114
+ const updatedState = existing ? { ...existing, duration: Math.max(existing.duration, totalFrames) } : { duration: totalFrames, deltas: [] };
8115
+ states[targetStateName] = updatedState;
8116
+ doc = { ...doc, states };
8117
+ (0, import_node_fs24.writeFileSync)(compositionPath, JSON.stringify(doc, null, 2), "utf-8");
8118
+ return {
8119
+ doc,
8120
+ addedVideoLayer: addedLayer,
8121
+ clipSlug,
8122
+ clipLayerId,
8123
+ targetStateName,
8124
+ targetDurationSeconds: clipDurationSeconds,
8125
+ ...transcribeResult !== void 0 ? { transcribe: transcribeResult } : {},
8126
+ ...mirroredTranscriptPath !== void 0 ? { mirroredTranscriptPath } : {}
8127
+ };
8128
+ }
8129
+ function canonicalSourceName(sourceFile, projectDir) {
8130
+ const abs = (0, import_node_path24.resolve)(sourceFile);
8131
+ const projectAbs = (0, import_node_path24.resolve)(projectDir);
8132
+ const ext = (0, import_node_path24.extname)(abs);
8133
+ if (POSSIBLE_SOURCE_EXTS.includes(ext.toLowerCase())) {
8134
+ const flat = `${projectAbs}/source${ext}`;
8135
+ if ((0, import_node_path24.resolve)(flat) === abs) return `source${ext}`;
8136
+ }
8137
+ if (abs.startsWith(`${projectAbs}/`)) {
8138
+ return abs.slice(projectAbs.length + 1);
8139
+ }
8140
+ return (0, import_node_path24.basename)(abs);
8141
+ }
8142
+ function lastTranscriptWordEnd(transcript) {
8143
+ if (!transcript) return void 0;
8144
+ let max = 0;
8145
+ for (const seg of transcript.segments) {
8146
+ for (const w of seg.words) {
8147
+ if (w.end > max) max = w.end;
8148
+ }
8149
+ }
8150
+ return max > 0 ? max : void 0;
8151
+ }
8152
+
8153
+ // src/lib/compose-image-project.ts
8154
+ var import_node_fs25 = require("fs");
8155
+ var import_node_path25 = require("path");
8156
+ async function composeImageProject(opts) {
8157
+ const projectDir = (0, import_node_path25.resolve)(opts.projectDir);
8158
+ const sourceAbs = (0, import_node_path25.resolve)(opts.sourceFile);
8159
+ const mediaBasename = opts.mediaBasename ?? (0, import_node_path25.basename)(sourceAbs);
8160
+ const mediaDir = `${projectDir}/media`;
8161
+ if (!(0, import_node_fs25.existsSync)(mediaDir)) (0, import_node_fs25.mkdirSync)(mediaDir, { recursive: true });
8162
+ const mediaDest = `${mediaDir}/${mediaBasename}`;
8163
+ if (!(0, import_node_fs25.existsSync)(mediaDest)) {
8164
+ (0, import_node_fs25.copyFileSync)(sourceAbs, mediaDest);
8165
+ }
8166
+ const compositionPath = (0, import_node_path25.join)(projectDir, opts.docPath);
8167
+ (0, import_node_fs25.mkdirSync)((0, import_node_path25.dirname)(compositionPath), { recursive: true });
8168
+ let doc;
8169
+ if ((0, import_node_fs25.existsSync)(compositionPath)) {
8170
+ doc = readAtelierDoc(compositionPath);
8171
+ } else {
8172
+ doc = {
8173
+ version: "1.0",
8174
+ name: (0, import_node_path25.basename)(projectDir),
8175
+ kind: "image",
8176
+ canvas: { width: 1080, height: 1920, fps: 30 },
8177
+ layers: [],
8178
+ states: { default: { duration: 1, deltas: [] } }
8179
+ };
8180
+ }
8181
+ const slug = slugifyBasename(mediaBasename);
8182
+ if (!slug) {
8183
+ throw new Error(
8184
+ `composeImageProject: cannot derive a slug from "${mediaBasename}"`
8185
+ );
8186
+ }
8187
+ const layerId = `img-${slug}`;
8188
+ const expectedSrc = `media/${mediaBasename}`;
8189
+ const existingImageLayer = doc.layers.find((l) => {
8190
+ if (!l.id.startsWith("img-")) return false;
8191
+ const v = l.visual;
8192
+ return v?.type === "image";
8193
+ });
8194
+ if (existingImageLayer) {
8195
+ const existingSrc = existingImageLayer.visual.src ?? "";
8196
+ const existingBasename = (0, import_node_path25.basename)(existingSrc);
8197
+ if (existingBasename !== mediaBasename) {
8198
+ throw new Error(
8199
+ "image project already has an image; rename or delete the existing image before adding another"
8200
+ );
8201
+ }
8202
+ return {
8203
+ doc,
8204
+ addedImageLayer: false,
8205
+ layerId: existingImageLayer.id,
8206
+ assetId: existingImageLayer.id
8207
+ };
8208
+ }
8209
+ const newLayer = {
8210
+ id: layerId,
8211
+ tags: ["image"],
8212
+ description: mediaBasename,
8213
+ visual: {
8214
+ type: "image",
8215
+ assetId: layerId,
8216
+ src: expectedSrc
8217
+ },
8218
+ frame: { x: 0, y: 0 },
8219
+ bounds: { width: doc.canvas.width, height: doc.canvas.height }
8220
+ };
8221
+ const assets = { ...doc.assets ?? {} };
8222
+ assets[layerId] = {
8223
+ type: "image",
8224
+ src: expectedSrc,
8225
+ description: mediaBasename
8226
+ };
8227
+ doc = {
8228
+ ...doc,
8229
+ layers: [...doc.layers, newLayer],
8230
+ assets
8231
+ };
8232
+ (0, import_node_fs25.writeFileSync)(compositionPath, JSON.stringify(doc, null, 2), "utf-8");
8233
+ return {
8234
+ doc,
8235
+ addedImageLayer: true,
8236
+ layerId,
8237
+ assetId: layerId
8238
+ };
8239
+ }
8240
+
8241
+ // src/lib/compose-carousel-project.ts
8242
+ var import_node_fs27 = require("fs");
8243
+ var import_node_path27 = require("path");
8244
+
8245
+ // src/lib/ref-cycle.ts
8246
+ var import_node_fs26 = require("fs");
8247
+ var import_node_path26 = require("path");
8248
+ var MAX_HOPS = 64;
8249
+ function detectCarouselCycle(rootPath, candidatePath, workspaceRoot) {
8250
+ const rootAbs = (0, import_node_path26.resolve)(rootPath);
8251
+ const candAbs = (0, import_node_path26.resolve)(candidatePath);
8252
+ const workspaceAbs = (0, import_node_path26.resolve)(workspaceRoot);
8253
+ if (rootAbs === candAbs) {
8254
+ return { cycle: true, path: [rootAbs, candAbs] };
8255
+ }
8256
+ const frontier = [{ abs: candAbs, chain: [rootAbs, candAbs] }];
8257
+ const visited = /* @__PURE__ */ new Set();
8258
+ let hops = 0;
8259
+ while (frontier.length > 0) {
8260
+ if (hops++ >= MAX_HOPS) {
8261
+ return { cycle: false };
8262
+ }
8263
+ const current = frontier.shift();
8264
+ if (visited.has(current.abs)) continue;
8265
+ visited.add(current.abs);
8266
+ if (!(0, import_node_fs26.existsSync)(current.abs)) {
8267
+ console.warn(`detectCarouselCycle: missing referenced file ${current.abs}`);
8268
+ continue;
8269
+ }
8270
+ let doc;
8271
+ try {
8272
+ const raw = (0, import_node_fs26.readFileSync)(current.abs, "utf-8");
8273
+ const parsed = parseAtelier(raw);
8274
+ if (parsed.success) {
8275
+ doc = parsed.data;
8276
+ } else {
8277
+ doc = JSON.parse(raw);
8278
+ }
8279
+ } catch (err) {
8280
+ console.warn(
8281
+ `detectCarouselCycle: failed to parse ${current.abs}: ${err.message}`
8282
+ );
8283
+ continue;
8284
+ }
8285
+ const refs = [];
8286
+ if (doc.slides) {
8287
+ for (const slide of doc.slides) {
8288
+ if (typeof slide.src === "string" && slide.src) refs.push(slide.src);
8289
+ }
8290
+ }
8291
+ if (doc.layers) {
8292
+ for (const layer of doc.layers) {
8293
+ const v = layer.visual;
8294
+ if (v && v.type === "ref") {
8295
+ const r = v;
8296
+ if (typeof r.src === "string" && r.src) refs.push(r.src);
8297
+ }
8298
+ }
8299
+ }
8300
+ for (const ref of refs) {
8301
+ const resolved = resolveRef(ref, current.abs, workspaceAbs);
8302
+ if (resolved === void 0) {
8303
+ console.warn(
8304
+ `detectCarouselCycle: missing referenced file "${ref}" (from ${current.abs})`
8305
+ );
8306
+ continue;
8307
+ }
8308
+ if (resolved === rootAbs || resolved === candAbs) {
8309
+ return { cycle: true, path: [...current.chain, resolved] };
8310
+ }
8311
+ if (!visited.has(resolved)) {
8312
+ frontier.push({ abs: resolved, chain: [...current.chain, resolved] });
8313
+ }
8314
+ }
8315
+ }
8316
+ return { cycle: false };
8317
+ }
8318
+ function resolveRef(ref, currentDocPath, workspaceRoot) {
8319
+ if ((0, import_node_path26.isAbsolute)(ref)) {
8320
+ return (0, import_node_fs26.existsSync)(ref) ? (0, import_node_path26.resolve)(ref) : void 0;
8321
+ }
8322
+ const docRelative = (0, import_node_path26.resolve)((0, import_node_path26.dirname)(currentDocPath), ref);
8323
+ if ((0, import_node_fs26.existsSync)(docRelative)) return docRelative;
8324
+ const workspaceRelative = (0, import_node_path26.resolve)(workspaceRoot, ref);
8325
+ if ((0, import_node_fs26.existsSync)(workspaceRelative)) return workspaceRelative;
8326
+ return void 0;
8327
+ }
8328
+
8329
+ // src/lib/compose-carousel-project.ts
8330
+ async function composeCarouselProject(opts) {
8331
+ const projectDir = (0, import_node_path27.resolve)(opts.projectDir);
8332
+ const slidePath = (0, import_node_path27.resolve)(opts.slidePath);
8333
+ const workspaceRoot = (0, import_node_path27.resolve)(opts.workspaceRoot ?? (0, import_node_path27.dirname)(projectDir));
8334
+ const compositionPath = (0, import_node_path27.join)(projectDir, opts.docPath);
8335
+ (0, import_node_fs27.mkdirSync)((0, import_node_path27.dirname)(compositionPath), { recursive: true });
8336
+ let doc;
8337
+ if ((0, import_node_fs27.existsSync)(compositionPath)) {
8338
+ doc = readAtelierDoc(compositionPath);
8339
+ } else {
8340
+ doc = {
8341
+ version: "1.0",
8342
+ name: (0, import_node_path27.basename)(projectDir),
8343
+ kind: "carousel",
8344
+ canvas: { width: 1080, height: 1920, fps: 30 },
8345
+ layers: [],
8346
+ states: { default: { duration: 1, deltas: [] } },
8347
+ slides: []
8348
+ };
8349
+ }
8350
+ if (doc.kind !== void 0 && doc.kind !== "carousel") {
8351
+ throw new Error("project is not a carousel; cannot add slides");
8352
+ }
8353
+ const cycleResult = detectCarouselCycle(
8354
+ compositionPath,
8355
+ slidePath,
8356
+ workspaceRoot
8357
+ );
8358
+ if (cycleResult.cycle) {
8359
+ const chain = cycleResult.path ?? [];
8360
+ throw new Error(`carousel cycle detected: ${chain.join(" \u2192 ")}`);
8361
+ }
8362
+ const computedSrc = (0, import_node_path27.relative)(workspaceRoot, slidePath);
8363
+ const slides = doc.slides ? [...doc.slides] : [];
8364
+ const existingIndex = slides.findIndex((s) => s.src === computedSrc);
8365
+ if (existingIndex >= 0) {
8366
+ if (!(0, import_node_fs27.existsSync)(compositionPath)) {
8367
+ doc = { ...doc, slides };
8368
+ (0, import_node_fs27.writeFileSync)(compositionPath, JSON.stringify(doc, null, 2), "utf-8");
8369
+ }
8370
+ return {
8371
+ doc: { ...doc, slides },
8372
+ addedSlide: false,
8373
+ slideIndex: existingIndex
8374
+ };
8375
+ }
8376
+ const slideRef = { src: computedSrc };
8377
+ if (opts.duration !== void 0) slideRef.duration = opts.duration;
8378
+ if (opts.transition !== void 0) slideRef.transition = opts.transition;
8379
+ if (opts.label !== void 0) slideRef.label = opts.label;
8380
+ let insertIndex;
8381
+ if (opts.insertAt !== void 0) {
8382
+ insertIndex = Math.max(0, Math.min(opts.insertAt, slides.length));
8383
+ slides.splice(insertIndex, 0, slideRef);
8384
+ } else {
8385
+ insertIndex = slides.length;
8386
+ slides.push(slideRef);
8387
+ }
8388
+ doc = { ...doc, slides };
8389
+ (0, import_node_fs27.writeFileSync)(compositionPath, JSON.stringify(doc, null, 2), "utf-8");
8390
+ return {
8391
+ doc,
8392
+ addedSlide: true,
8393
+ slideIndex: insertIndex
8394
+ };
8395
+ }
8396
+
8397
+ // src/lib/ingest-dispatch.ts
8398
+ var import_node_fs28 = require("fs");
8399
+ var import_node_path28 = require("path");
8400
+ var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".mov", ".webm", ".mkv", ".avi"]);
8401
+ var IMAGE_EXTS2 = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".webp", ".gif"]);
8402
+ var AUDIO_EXTS = /* @__PURE__ */ new Set([".mp3", ".wav", ".m4a", ".aac", ".flac", ".ogg"]);
8403
+ function classifyMediaFile(filename) {
8404
+ const ext = (0, import_node_path28.extname)(filename).toLowerCase();
8405
+ if (!ext) return "unsupported";
8406
+ if (VIDEO_EXTS.has(ext)) return "video";
8407
+ if (IMAGE_EXTS2.has(ext)) return "image";
8408
+ if (AUDIO_EXTS.has(ext)) return "audio";
8409
+ return "unsupported";
8410
+ }
8411
+ function routeIngest(filename) {
8412
+ const kind = classifyMediaFile(filename);
8413
+ if (kind === "unsupported") {
8414
+ return { kind, target: "reject", error: "file type not supported" };
8415
+ }
8416
+ return { kind, target: "add-to-bin" };
8417
+ }
8418
+ function routeIngestWithTarget(filename, targetDocPath, targetDocKind) {
8419
+ const kind = classifyMediaFile(filename);
8420
+ if (kind === "unsupported") {
8421
+ return { kind, target: "reject", error: "file type not supported" };
8422
+ }
8423
+ if (targetDocKind === void 0) {
8424
+ return {
8425
+ kind,
8426
+ target: "reject",
8427
+ error: "target doc kind unresolved"
8428
+ };
8429
+ }
8430
+ if (kind === "video") {
8431
+ if (targetDocKind === "video") {
8432
+ return { kind, target: "compose-video", docPath: targetDocPath };
8433
+ }
8434
+ return {
8435
+ kind,
8436
+ target: "reject",
8437
+ error: targetDocKind === "image" ? "cannot add video clip to an image doc" : "cannot add video clip to a carousel doc"
8438
+ };
8439
+ }
8440
+ if (kind === "audio") {
8441
+ if (targetDocKind === "video") {
8442
+ return { kind, target: "add-audio-clip", docPath: targetDocPath };
8443
+ }
8444
+ return {
8445
+ kind,
8446
+ target: "reject",
8447
+ error: targetDocKind === "image" ? "cannot add audio to an image doc" : "cannot add audio to a carousel doc"
8448
+ };
8449
+ }
8450
+ return {
8451
+ kind,
8452
+ target: "reject",
8453
+ error: "image drag-drop into an existing doc not supported in v1"
8454
+ };
8455
+ }
8456
+
8457
+ // src/lib/doc-management.ts
8458
+ var import_node_fs29 = require("fs");
8459
+ var import_node_path29 = require("path");
8460
+
8461
+ // src/lib/doc-management-types.ts
8462
+ function isValidDocName(name) {
8463
+ if (typeof name !== "string") return false;
8464
+ const trimmed = name.trim();
8465
+ if (trimmed.length === 0) return false;
8466
+ if (trimmed.length > 200) return false;
8467
+ if (trimmed.startsWith(".")) return false;
8468
+ if (trimmed.includes("/") || trimmed.includes("\\") || trimmed.includes("..")) return false;
8469
+ if (/[\x00-\x1f]/.test(trimmed)) return false;
8470
+ return true;
8471
+ }
8472
+
8473
+ // src/lib/doc-management.ts
8474
+ var MAX_COLLISION_ATTEMPTS = 999;
8475
+ async function createDoc(opts) {
8476
+ if (!isValidDocName(opts.name)) {
8477
+ throw new Error("invalid doc name");
8478
+ }
8479
+ const slug = slugifyBasename(opts.name);
8480
+ if (!slug) {
8481
+ throw new Error("invalid doc name");
8482
+ }
8483
+ const projectDir = (0, import_node_path29.resolve)(opts.projectDir);
8484
+ const subdir = opts.asCarouselSlide ? "slides" : "";
8485
+ const subdirAbs = subdir ? (0, import_node_path29.join)(projectDir, subdir) : projectDir;
8486
+ let chosenName;
8487
+ let chosenAbs;
8488
+ for (let attempt = 0; attempt <= MAX_COLLISION_ATTEMPTS; attempt++) {
8489
+ const candidateStem = attempt === 0 ? slug : `${slug}-${attempt}`;
8490
+ const candidateName = `${candidateStem}.atelier`;
8491
+ const candidateAbs = (0, import_node_path29.join)(subdirAbs, candidateName);
8492
+ if (!(0, import_node_fs29.existsSync)(candidateAbs)) {
8493
+ chosenName = candidateName;
8494
+ chosenAbs = candidateAbs;
8495
+ break;
8496
+ }
8497
+ }
8498
+ if (!chosenName || !chosenAbs) {
8499
+ throw new Error("collision suffix exhausted (999 attempts)");
8500
+ }
8501
+ const docPath = subdir ? `${subdir}/${chosenName}` : chosenName;
8502
+ const docName = chosenName.slice(0, -".atelier".length);
8503
+ const doc = scaffoldDoc(docName, opts.kind);
8504
+ if (subdir) {
8505
+ (0, import_node_fs29.mkdirSync)(subdirAbs, { recursive: true });
8506
+ }
8507
+ (0, import_node_fs29.writeFileSync)(chosenAbs, JSON.stringify(doc, null, 2), "utf-8");
8508
+ let appendedSlideIndex;
8509
+ if (opts.asCarouselSlide && opts.carouselDocPath) {
8510
+ const carouselAbs = (0, import_node_path29.isAbsolute)(opts.carouselDocPath) ? opts.carouselDocPath : (0, import_node_path29.join)(projectDir, opts.carouselDocPath);
8511
+ const carouselProjectDir = (0, import_node_path29.dirname)(carouselAbs);
8512
+ const carouselDocRel = (0, import_node_path29.relative)(carouselProjectDir, carouselAbs);
8513
+ try {
8514
+ const composeOpts = {
8515
+ projectDir: carouselProjectDir,
8516
+ docPath: carouselDocRel,
8517
+ slidePath: chosenAbs
8518
+ };
8519
+ if (opts.insertAt !== void 0) composeOpts.insertAt = opts.insertAt;
8520
+ const composeResult = await composeCarouselProject(composeOpts);
8521
+ appendedSlideIndex = composeResult.slideIndex;
8522
+ } catch (err) {
8523
+ try {
8524
+ (0, import_node_fs29.unlinkSync)(chosenAbs);
8525
+ } catch {
8526
+ }
8527
+ throw err;
8528
+ }
8529
+ }
8530
+ return {
8531
+ doc,
8532
+ docPath,
8533
+ ...appendedSlideIndex !== void 0 ? { appendedSlideIndex } : {}
8534
+ };
8535
+ }
8536
+ function scaffoldDoc(name, kind) {
8537
+ const base = {
8538
+ version: "1.0",
8539
+ name,
8540
+ kind,
8541
+ canvas: { width: 1080, height: 1920, fps: 30 },
8542
+ layers: [],
8543
+ states: { default: { duration: 1, deltas: [] } }
8544
+ };
8545
+ if (kind === "carousel") {
8546
+ base.slides = [];
8547
+ }
8548
+ return base;
8549
+ }
8550
+ async function duplicateDoc(opts) {
8551
+ const projectDir = (0, import_node_path29.resolve)(opts.projectDir);
8552
+ const sourceAbs = (0, import_node_path29.join)(projectDir, opts.sourceDocPath);
8553
+ if (!(0, import_node_fs29.existsSync)(sourceAbs)) {
8554
+ throw new Error("source doc not found");
8555
+ }
8556
+ const doc = readAtelierDoc(sourceAbs);
8557
+ const sourceDir = (0, import_node_path29.dirname)(sourceAbs);
8558
+ const sourceBase = (0, import_node_path29.basename)(sourceAbs);
8559
+ const sourceExt = (0, import_node_path29.extname)(sourceBase);
8560
+ const sourceStem = sourceExt ? sourceBase.slice(0, -sourceExt.length) : sourceBase;
8561
+ let chosenAbs;
8562
+ let chosenStem;
8563
+ for (let attempt = 1; attempt <= MAX_COLLISION_ATTEMPTS; attempt++) {
8564
+ const candidateStem = attempt === 1 ? `${sourceStem}-copy` : `${sourceStem}-copy-${attempt}`;
8565
+ const candidateAbs = (0, import_node_path29.join)(sourceDir, `${candidateStem}${sourceExt}`);
8566
+ if (!(0, import_node_fs29.existsSync)(candidateAbs)) {
8567
+ chosenAbs = candidateAbs;
8568
+ chosenStem = candidateStem;
8569
+ break;
8570
+ }
8571
+ }
8572
+ if (!chosenAbs || !chosenStem) {
8573
+ throw new Error("collision suffix exhausted (999 attempts)");
8574
+ }
8575
+ const newDocPath = sourceRelDir(opts.sourceDocPath) ? `${sourceRelDir(opts.sourceDocPath)}/${chosenStem}${sourceExt}` : `${chosenStem}${sourceExt}`;
8576
+ (0, import_node_fs29.writeFileSync)(chosenAbs, JSON.stringify(doc, null, 2), "utf-8");
8577
+ let appendedSlideIndex;
8578
+ if (opts.appendSlideRef) {
8579
+ const sourceDirRel = sourceRelDir(opts.sourceDocPath);
8580
+ if (sourceDirRel !== "slides") {
8581
+ try {
8582
+ (0, import_node_fs29.unlinkSync)(chosenAbs);
8583
+ } catch {
8584
+ }
8585
+ throw new Error("appendSlideRef requires a slide source");
8586
+ }
8587
+ const manifestPath = (0, import_node_path29.join)(projectDir, "project.atelier");
8588
+ let sourceIndex = -1;
8589
+ if ((0, import_node_fs29.existsSync)(manifestPath)) {
8590
+ try {
8591
+ const manifest = readAtelierDoc(manifestPath);
8592
+ if (manifest.kind === "carousel" && Array.isArray(manifest.slides)) {
8593
+ const workspaceRoot = (0, import_node_path29.dirname)(projectDir);
8594
+ for (let i = 0; i < manifest.slides.length; i++) {
8595
+ const ref = manifest.slides[i];
8596
+ const resolved = resolveSlideRef(ref.src, projectDir, workspaceRoot);
8597
+ if (resolved === sourceAbs) {
8598
+ sourceIndex = i;
8599
+ break;
8600
+ }
8601
+ }
8602
+ }
8603
+ } catch {
8604
+ }
8605
+ }
8606
+ try {
8607
+ const composeOpts = {
8608
+ projectDir,
8609
+ docPath: "project.atelier",
8610
+ slidePath: chosenAbs
8611
+ };
8612
+ if (sourceIndex >= 0) composeOpts.insertAt = sourceIndex + 1;
8613
+ const composeResult = await composeCarouselProject(composeOpts);
8614
+ appendedSlideIndex = composeResult.slideIndex;
8615
+ } catch (err) {
8616
+ try {
8617
+ (0, import_node_fs29.unlinkSync)(chosenAbs);
8618
+ } catch {
8619
+ }
8620
+ throw err;
8621
+ }
8622
+ }
8623
+ return {
8624
+ sourceDocPath: opts.sourceDocPath,
8625
+ newDocPath,
8626
+ ...appendedSlideIndex !== void 0 ? { appendedSlideIndex } : {}
8627
+ };
8628
+ }
8629
+ function sourceRelDir(relPath) {
8630
+ const dir = (0, import_node_path29.dirname)(relPath);
8631
+ return dir === "." ? "" : dir;
8632
+ }
8633
+ function deleteDoc(opts) {
8634
+ const workspaceAbs = (0, import_node_path29.resolve)(opts.workspaceDir);
8635
+ const projectDir = (0, import_node_path29.join)(workspaceAbs, opts.projectName);
8636
+ const docAbs = (0, import_node_path29.join)(projectDir, opts.docPath);
8637
+ if (!(0, import_node_fs29.existsSync)(docAbs)) {
8638
+ throw new Error("doc not found");
8639
+ }
8640
+ const slideRefsRemoved = [];
8641
+ const refLayersDangling = [];
8642
+ let entries;
8643
+ try {
8644
+ entries = (0, import_node_fs29.readdirSync)(workspaceAbs);
8645
+ } catch {
8646
+ entries = [];
8647
+ }
8648
+ for (const entry of entries) {
8649
+ if (entry.startsWith(".")) continue;
8650
+ const otherProjectDir = (0, import_node_path29.join)(workspaceAbs, entry);
8651
+ let st;
8652
+ try {
8653
+ st = (0, import_node_fs29.statSync)(otherProjectDir);
8654
+ } catch {
8655
+ continue;
8656
+ }
8657
+ if (!st.isDirectory()) continue;
8658
+ const projectDocs = enumerateProjectAtelierDocs(otherProjectDir);
8659
+ for (const { absPath, relPath } of projectDocs) {
8660
+ if (absPath === docAbs) continue;
8661
+ let otherDoc;
8662
+ try {
8663
+ otherDoc = readAtelierDoc(absPath);
8664
+ } catch {
8665
+ continue;
8666
+ }
8667
+ let docMutated = false;
8668
+ if (otherDoc.kind === "carousel" && Array.isArray(otherDoc.slides)) {
8669
+ const slides = otherDoc.slides;
8670
+ const matchIndices = [];
8671
+ for (let i = 0; i < slides.length; i++) {
8672
+ const ref = slides[i];
8673
+ const resolved = resolveSlideRef(ref.src, otherProjectDir, workspaceAbs);
8674
+ if (resolved === docAbs) {
8675
+ matchIndices.push(i);
8676
+ slideRefsRemoved.push({ project: entry, slideIndex: i });
8677
+ }
8678
+ }
8679
+ if (matchIndices.length > 0) {
8680
+ const matchSet = new Set(matchIndices);
8681
+ const kept = slides.filter((_, i) => !matchSet.has(i));
8682
+ otherDoc = { ...otherDoc, slides: kept };
8683
+ docMutated = true;
8684
+ }
8685
+ }
8686
+ if (Array.isArray(otherDoc.layers)) {
8687
+ for (const layer of otherDoc.layers) {
8688
+ const v = layer.visual;
8689
+ if (v && v.type === "ref") {
8690
+ const refVisual = v;
8691
+ if (typeof refVisual.src === "string" && refVisual.src.length > 0) {
8692
+ const resolved = resolveSlideRef(refVisual.src, otherProjectDir, workspaceAbs);
8693
+ if (resolved === docAbs) {
8694
+ refLayersDangling.push({
8695
+ project: entry,
8696
+ docPath: relPath,
8697
+ layerId: layer.id
8698
+ });
8699
+ }
8700
+ }
8701
+ }
8702
+ }
8703
+ }
8704
+ if (docMutated) {
8705
+ (0, import_node_fs29.writeFileSync)(absPath, JSON.stringify(otherDoc, null, 2), "utf-8");
8706
+ }
8707
+ }
8708
+ }
8709
+ (0, import_node_fs29.unlinkSync)(docAbs);
8710
+ return {
8711
+ docPath: opts.docPath,
8712
+ slideRefsRemoved,
8713
+ refLayersDangling
8714
+ };
8715
+ }
8716
+ function resolveSlideRef(src, ownerProjectDir, workspaceRoot) {
8717
+ if ((0, import_node_path29.isAbsolute)(src)) {
8718
+ return (0, import_node_fs29.existsSync)(src) ? (0, import_node_path29.resolve)(src) : void 0;
8719
+ }
8720
+ const projectRelative = (0, import_node_path29.resolve)(ownerProjectDir, src);
8721
+ if ((0, import_node_fs29.existsSync)(projectRelative)) return projectRelative;
8722
+ const workspaceRelative = (0, import_node_path29.resolve)(workspaceRoot, src);
8723
+ if ((0, import_node_fs29.existsSync)(workspaceRelative)) return workspaceRelative;
8724
+ return void 0;
8725
+ }
8726
+ function listDocs(projectDir) {
8727
+ const projectAbs = (0, import_node_path29.resolve)(projectDir);
8728
+ const found = enumerateProjectAtelierDocs(projectAbs);
8729
+ const out = found.map(({ absPath, relPath }) => {
8730
+ let kind;
8731
+ try {
8732
+ const doc = readAtelierDoc(absPath);
8733
+ kind = doc.kind;
8734
+ } catch {
8735
+ kind = void 0;
8736
+ }
8737
+ const entry = { path: relPath };
8738
+ if (kind !== void 0) entry.kind = kind;
8739
+ return entry;
8740
+ }).sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
8741
+ return out;
8742
+ }
8743
+ function enumerateProjectAtelierDocs(projectAbs) {
8744
+ const out = [];
8745
+ for (const sub of ["", "slides"]) {
8746
+ const dirAbs = sub ? (0, import_node_path29.join)(projectAbs, sub) : projectAbs;
8747
+ let entries;
8748
+ try {
8749
+ entries = (0, import_node_fs29.readdirSync)(dirAbs);
8750
+ } catch {
8751
+ continue;
8752
+ }
8753
+ for (const name of entries) {
8754
+ if (!name.endsWith(".atelier")) continue;
8755
+ if (name === ".atelier") continue;
8756
+ const abs = (0, import_node_path29.join)(dirAbs, name);
8757
+ try {
8758
+ const st = (0, import_node_fs29.statSync)(abs);
8759
+ if (!st.isFile()) continue;
8760
+ } catch {
8761
+ continue;
8762
+ }
8763
+ const relPath = sub ? `${sub}/${name}` : name;
8764
+ out.push({ absPath: abs, relPath });
8765
+ }
8766
+ }
8767
+ return out;
8768
+ }
5827
8769
  // Annotate the CommonJS export names for ESM import in node:
5828
8770
  0 && (module.exports = {
8771
+ ARTIFACT_FILENAMES,
5829
8772
  CanvasUnavailableError,
8773
+ LEARNING_MODES,
8774
+ LEARNING_MODE_VERSION,
5830
8775
  RECIPE_VERSION,
5831
8776
  VIDEO_CUTLIST_VERSION,
5832
8777
  VIDEO_PROJECT_VERSION,
@@ -5841,6 +8786,7 @@ Done.`);
5841
8786
  applyRecipeToTrimOptions,
5842
8787
  applySplit,
5843
8788
  applyTextEdit,
8789
+ artifactsCommand,
5844
8790
  assetsCommand,
5845
8791
  buildCaptionLayers,
5846
8792
  buildFfmpegArgs,
@@ -5848,8 +8794,15 @@ Done.`);
5848
8794
  carouselCommand,
5849
8795
  carouselFileName,
5850
8796
  checkFfmpeg,
8797
+ classifyMediaFile,
5851
8798
  composeCarouselFrameDoc,
8799
+ composeCarouselProject,
8800
+ composeImageProject,
8801
+ composeVideoProject,
8802
+ createDoc,
5852
8803
  createVideoProject,
8804
+ deleteDoc,
8805
+ duplicateDoc,
5853
8806
  effectiveSpan,
5854
8807
  expandInputs,
5855
8808
  exportImageCommand,
@@ -5862,26 +8815,38 @@ Done.`);
5862
8815
  getVariables,
5863
8816
  groupIntoPhrases,
5864
8817
  infoCommand,
8818
+ initCommand,
8819
+ isInsideGitRepo,
8820
+ isValidDocName,
8821
+ learningModePath,
8822
+ listDocs,
8823
+ loadArtifactsFromProject,
5865
8824
  loadCanvasModule,
5866
8825
  loadRecipe,
5867
8826
  loadVideoProject,
5868
8827
  mergeTranscriptWithExisting,
8828
+ parseModeAnswer,
5869
8829
  parseWhisperCppJson,
5870
8830
  probeWhisper,
5871
8831
  readComposition,
5872
8832
  readCutList,
8833
+ readLearningMode,
5873
8834
  readTranscript,
5874
8835
  recipeCommand,
5875
8836
  recipeToYaml,
5876
8837
  renderCommand,
5877
8838
  renderDocument,
5878
8839
  renderDocumentToPng,
8840
+ renderLearningModeYaml,
5879
8841
  renderRecipeWithDefaults,
5880
8842
  resolveExportDimensions,
5881
8843
  resolveRecipePath,
5882
8844
  resolveStill,
5883
8845
  rewriteCaptionLayers,
5884
8846
  rewriteCutLayers,
8847
+ routeIngest,
8848
+ routeIngestWithTarget,
8849
+ runInit,
5885
8850
  runWhisperCpp,
5886
8851
  scaffoldRecipeYaml,
5887
8852
  stillCommand,
@@ -5895,6 +8860,7 @@ Done.`);
5895
8860
  variablesCommand,
5896
8861
  writeComposition,
5897
8862
  writeCutList,
8863
+ writeLearningMode,
5898
8864
  writeTranscript
5899
8865
  });
5900
8866
  //# sourceMappingURL=index.cjs.map