@a-company/atelier 0.37.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.
- package/dist/{chunk-JPZ4F4PW.js → chunk-3ARBOSWY.js} +64 -5
- package/dist/chunk-3ARBOSWY.js.map +1 -0
- package/dist/cli.js +11469 -413
- package/dist/cli.js.map +1 -1
- package/dist/{dist-M67UZGFQ.js → dist-3YQK6PI6.js} +2 -2
- package/dist/index.cjs +3193 -227
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +701 -8
- package/dist/index.d.ts +701 -8
- package/dist/index.js +7237 -72
- package/dist/index.js.map +1 -1
- package/dist/mcp.js +2898 -507
- package/dist/mcp.js.map +1 -1
- package/package.json +6 -6
- package/src/web/inline-app.ts +55 -4
- package/src/web/timeline-state-types.ts +28 -0
- package/src/web/timeline-view.test.ts +99 -0
- package/src/web/timeline-view.ts +339 -0
- package/src/web/workspace-app.ts +3146 -0
- package/templates/workspace/.claude/agents/atelier-iris.md +75 -0
- package/templates/workspace/.claude/agents/atelier-lux.md +67 -0
- package/templates/workspace/.claude/agents/atelier-quill.md +61 -0
- package/templates/workspace/.gitignore +30 -0
- package/templates/workspace/.paradigm/personas/_shared/cascade-merge.md +172 -0
- package/templates/workspace/CLAUDE.md +93 -0
- package/templates/workspace/README.md +75 -0
- package/templates/workspace/SETUP.md +127 -0
- package/templates/workspace/_brand/.atelier-brand.yaml +34 -0
- package/templates/workspace/_brand/DESIGN.md +56 -0
- package/templates/workspace/_brand/SCRIPT.md +41 -0
- package/templates/workspace/_brand/STORYBOARD.md +33 -0
- package/templates/workspace/_packs/README.md +54 -0
- package/templates/workspace/projects/README.md +49 -0
- package/templates/workspace/workspace.atelier +22 -0
- package/university/index.yaml +1 -1
- package/dist/chunk-5QQESXI6.js +0 -4432
- package/dist/chunk-5QQESXI6.js.map +0 -1
- package/dist/chunk-JPZ4F4PW.js.map +0 -1
- package/dist/cli.cjs +0 -6313
- package/dist/cli.cjs.map +0 -1
- package/dist/cli.d.cts +0 -1
- package/dist/cli.d.ts +0 -1
- package/dist/mcp.cjs +0 -5462
- package/dist/mcp.cjs.map +0 -1
- /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,
|
|
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
|
-
|
|
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.
|
|
1159
|
-
|
|
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
|
|
1425
|
-
__export(
|
|
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(
|
|
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 =
|
|
1943
|
-
noise:
|
|
1944
|
-
min_silence:
|
|
1945
|
-
default_padding_pre:
|
|
1946
|
-
default_padding_post:
|
|
1947
|
-
match_tolerance:
|
|
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 =
|
|
1950
|
-
font_family:
|
|
1951
|
-
font_size:
|
|
1952
|
-
font_weight:
|
|
1953
|
-
text_align:
|
|
1954
|
-
color:
|
|
1955
|
-
y_ratio:
|
|
1956
|
-
width_ratio:
|
|
1957
|
-
fade_seconds:
|
|
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 =
|
|
1960
|
-
max_words:
|
|
1961
|
-
pause_gap:
|
|
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 =
|
|
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 =
|
|
1970
|
-
font_family:
|
|
1971
|
-
font_size:
|
|
1972
|
-
font_weight:
|
|
1973
|
-
color:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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 =
|
|
2017
|
-
text:
|
|
2174
|
+
var OverlayHandleRuleSchema = import_zod18.z.object({
|
|
2175
|
+
text: import_zod18.z.string().min(1),
|
|
2018
2176
|
anchor: OverlayAnchorSchema,
|
|
2019
|
-
margin:
|
|
2177
|
+
margin: import_zod18.z.number().nonnegative().optional(),
|
|
2020
2178
|
style: OverlayTextStyleSchema.optional()
|
|
2021
2179
|
}).strict();
|
|
2022
|
-
var OverlayPageNumberRuleSchema =
|
|
2023
|
-
format:
|
|
2180
|
+
var OverlayPageNumberRuleSchema = import_zod18.z.object({
|
|
2181
|
+
format: import_zod18.z.string().superRefine(validatePageNumberFormat),
|
|
2024
2182
|
anchor: OverlayAnchorSchema,
|
|
2025
|
-
margin:
|
|
2183
|
+
margin: import_zod18.z.number().nonnegative().optional(),
|
|
2026
2184
|
style: OverlayTextStyleSchema.optional()
|
|
2027
2185
|
}).strict();
|
|
2028
|
-
var OverlayRulesSchema =
|
|
2186
|
+
var OverlayRulesSchema = import_zod18.z.object({
|
|
2029
2187
|
handle: OverlayHandleRuleSchema.optional(),
|
|
2030
2188
|
page_number: OverlayPageNumberRuleSchema.optional()
|
|
2031
2189
|
}).strict();
|
|
2032
|
-
var StudioRecipeSchema =
|
|
2033
|
-
version:
|
|
2034
|
-
name:
|
|
2035
|
-
description:
|
|
2036
|
-
author:
|
|
2037
|
-
tags:
|
|
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:
|
|
2045
|
-
transition_kit:
|
|
2046
|
-
palette:
|
|
2047
|
-
audio_policy:
|
|
2048
|
-
aspect_targets:
|
|
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
|
|
2332
|
-
var
|
|
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((
|
|
3063
|
+
return new Promise((resolve28) => {
|
|
2340
3064
|
const proc = (0, import_node_child_process.spawn)("ffmpeg", ["-version"], { stdio: "pipe" });
|
|
2341
|
-
proc.on("error", () =>
|
|
2342
|
-
proc.on("close", (code) =>
|
|
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((
|
|
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
|
|
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
|
-
(
|
|
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((
|
|
2500
|
-
ffmpeg.on("close",
|
|
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,
|
|
3314
|
+
const absPath = (0, import_node_path5.resolve)(file);
|
|
2520
3315
|
let content;
|
|
2521
3316
|
try {
|
|
2522
|
-
content = (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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
|
|
2605
|
-
var
|
|
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,
|
|
3871
|
+
const absPath = (0, import_node_path6.resolve)(file);
|
|
3071
3872
|
let content;
|
|
3072
3873
|
try {
|
|
3073
|
-
content = (0,
|
|
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,
|
|
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
|
|
3129
|
-
var
|
|
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
|
-
|
|
3624
|
-
|
|
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,
|
|
4502
|
+
const absPath = (0, import_node_path7.resolve)(file);
|
|
3699
4503
|
let content;
|
|
3700
4504
|
try {
|
|
3701
|
-
content = (0,
|
|
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,
|
|
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
|
|
3743
|
-
var
|
|
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((
|
|
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,
|
|
4698
|
+
const absPath = (0, import_node_path8.resolve)(file);
|
|
3895
4699
|
let content;
|
|
3896
4700
|
try {
|
|
3897
|
-
content = (0,
|
|
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,
|
|
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
|
|
3956
|
-
var
|
|
4759
|
+
var import_node_fs10 = require("fs");
|
|
4760
|
+
var import_node_path10 = require("path");
|
|
3957
4761
|
|
|
3958
4762
|
// src/lib/recipe.ts
|
|
3959
|
-
var
|
|
3960
|
-
var
|
|
4763
|
+
var import_node_fs9 = require("fs");
|
|
4764
|
+
var import_node_path9 = require("path");
|
|
3961
4765
|
var import_node_os = require("os");
|
|
3962
|
-
var
|
|
4766
|
+
var import_yaml3 = require("yaml");
|
|
3963
4767
|
var RECIPE_VERSION = "1.0";
|
|
3964
4768
|
function resolveRecipePath(pathOrName, projectDir) {
|
|
3965
|
-
if ((0,
|
|
3966
|
-
return (0,
|
|
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,
|
|
3972
|
-
for (const ext of exts) candidates.push((0,
|
|
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,
|
|
3975
|
-
for (const ext of exts) candidates.push((0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
5063
|
+
const abs = (0, import_node_path10.resolve)(pattern);
|
|
4260
5064
|
let isDir = false;
|
|
4261
5065
|
try {
|
|
4262
|
-
isDir = (0,
|
|
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,
|
|
4270
|
-
const base = (0,
|
|
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,
|
|
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,
|
|
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,
|
|
5090
|
+
entries = (0, import_node_fs10.readdirSync)(dir);
|
|
4287
5091
|
} catch {
|
|
4288
5092
|
return [];
|
|
4289
5093
|
}
|
|
4290
|
-
return entries.filter(isImageFile).map((name) => (0,
|
|
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,
|
|
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, `[^${
|
|
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,
|
|
4328
|
-
const stem = (0,
|
|
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,
|
|
5175
|
+
const outDir = (0, import_node_path10.resolve)(options.outDir);
|
|
4372
5176
|
try {
|
|
4373
|
-
(0,
|
|
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,
|
|
4400
|
-
console.log(` [${index}/${total}] ${(0,
|
|
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
|
|
4419
|
-
var
|
|
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) =>
|
|
4424
|
-
|
|
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,
|
|
5253
|
+
const absPath = (0, import_node_path11.resolve)(file);
|
|
4452
5254
|
let content;
|
|
4453
5255
|
try {
|
|
4454
|
-
content = (0,
|
|
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
|
|
4479
|
-
var
|
|
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,
|
|
5317
|
+
const absPath = (0, import_node_path12.resolve)(file);
|
|
4516
5318
|
let content;
|
|
4517
5319
|
try {
|
|
4518
|
-
content = (0,
|
|
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
|
|
4543
|
-
var
|
|
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,
|
|
4555
|
-
const ext = (0,
|
|
4556
|
-
const stem = (0,
|
|
4557
|
-
const projectDir = destDir ? (0,
|
|
4558
|
-
if (!(0,
|
|
4559
|
-
(0,
|
|
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,
|
|
4563
|
-
if (!(0,
|
|
4564
|
-
(0,
|
|
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,
|
|
4567
|
-
const transcriptPath = (0,
|
|
4568
|
-
const cutsPath = (0,
|
|
4569
|
-
const exportDir = (0,
|
|
4570
|
-
if (!(0,
|
|
4571
|
-
(0,
|
|
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,
|
|
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,
|
|
4617
|
-
(0,
|
|
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,
|
|
4625
|
-
(0,
|
|
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,
|
|
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,
|
|
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,
|
|
4659
|
-
compositionPath: (0,
|
|
4660
|
-
transcriptPath: (0,
|
|
4661
|
-
cutsPath: (0,
|
|
4662
|
-
exportDir: (0,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
4693
|
-
const raw = JSON.parse((0,
|
|
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,
|
|
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,
|
|
5522
|
+
return JSON.parse((0, import_node_fs13.readFileSync)(project.compositionPath, "utf-8"));
|
|
4721
5523
|
}
|
|
4722
5524
|
function writeComposition(project, doc) {
|
|
4723
|
-
(0,
|
|
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((
|
|
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
|
|
5636
|
+
else resolve28(stdout);
|
|
4835
5637
|
});
|
|
4836
5638
|
});
|
|
4837
5639
|
}
|
|
4838
5640
|
function runCaptureStderr(cmd, args) {
|
|
4839
|
-
return new Promise((
|
|
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", () =>
|
|
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
|
|
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
|
-
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5068
|
-
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
|
|
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
|
|
5889
|
+
return cached;
|
|
5079
5890
|
}
|
|
5080
|
-
function
|
|
5081
|
-
const
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
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,
|
|
6040
|
+
return (0, import_node_fs14.existsSync)(name);
|
|
5122
6041
|
}
|
|
5123
|
-
return new Promise((
|
|
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", () =>
|
|
5128
|
-
probe.on("close", (code) =>
|
|
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((
|
|
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
|
-
|
|
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
|
|
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
|
|
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: [
|
|
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
|
|
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
|
|
5723
|
-
var
|
|
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,
|
|
5729
|
-
if (!(0,
|
|
5730
|
-
(0,
|
|
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,
|
|
5735
|
-
if ((0,
|
|
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,
|
|
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
|