@a-company/atelier 0.28.2 → 0.36.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-5QQESXI6.js +4432 -0
- package/dist/chunk-5QQESXI6.js.map +1 -0
- package/dist/{chunk-C5DBTHXB.js → chunk-JPZ4F4PW.js} +44 -2
- package/dist/chunk-JPZ4F4PW.js.map +1 -0
- package/dist/cli.cjs +2577 -529
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +405 -433
- package/dist/cli.js.map +1 -1
- package/dist/{dist-6IHF7WA7.js → dist-M67UZGFQ.js} +2 -2
- package/dist/index.cjs +2296 -40
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +584 -2
- package/dist/index.d.ts +584 -2
- package/dist/index.js +112 -4
- package/dist/mcp.cjs +1247 -367
- package/dist/mcp.cjs.map +1 -1
- package/dist/mcp.js +1241 -367
- package/dist/mcp.js.map +1 -1
- package/package.json +15 -9
- package/src/web/inline-app.ts +867 -0
- package/src/web/tsconfig.json +9 -0
- package/templates/welcome.atelier +67 -0
- package/dist/chunk-C5DBTHXB.js.map +0 -1
- package/dist/chunk-LC7ICNMN.js +0 -2242
- package/dist/chunk-LC7ICNMN.js.map +0 -1
- /package/dist/{dist-6IHF7WA7.js.map → dist-M67UZGFQ.js.map} +0 -0
package/dist/cli.cjs
CHANGED
|
@@ -420,7 +420,18 @@ function resolveFrame(doc, stateName, frame, overrideDeltas) {
|
|
|
420
420
|
}
|
|
421
421
|
}
|
|
422
422
|
}
|
|
423
|
-
|
|
423
|
+
const resolvedLayer = { id: layer.id, layer, computedProperties };
|
|
424
|
+
if (layer.visual.type === "video") {
|
|
425
|
+
const video = layer.visual;
|
|
426
|
+
const fps = doc.canvas.fps;
|
|
427
|
+
const startFrame = video.startFrame ?? 0;
|
|
428
|
+
const sourceOffset = video.sourceOffset ?? 0;
|
|
429
|
+
const playbackRate = video.playbackRate ?? 1;
|
|
430
|
+
const relativeFrame = Math.max(0, frame - startFrame);
|
|
431
|
+
const sourceTime = relativeFrame / fps * playbackRate + sourceOffset;
|
|
432
|
+
resolvedLayer.videoSourceTime = video.sourceEnd !== void 0 ? Math.min(sourceTime, video.sourceEnd) : sourceTime;
|
|
433
|
+
}
|
|
434
|
+
return resolvedLayer;
|
|
424
435
|
});
|
|
425
436
|
return { frame, stateName, layers: resolvedLayers };
|
|
426
437
|
}
|
|
@@ -1027,6 +1038,14 @@ function renderImage(ctx, eff, imageCache) {
|
|
|
1027
1038
|
}
|
|
1028
1039
|
ctx.drawImage(img, 0, 0, eff.width, eff.height);
|
|
1029
1040
|
}
|
|
1041
|
+
function renderVideo(ctx, eff, sourceTime, provider) {
|
|
1042
|
+
const visual = eff.visual;
|
|
1043
|
+
const src = visual.src;
|
|
1044
|
+
if (!src) return;
|
|
1045
|
+
const frame = provider(src, sourceTime, eff.width, eff.height);
|
|
1046
|
+
if (!frame) return;
|
|
1047
|
+
ctx.drawImage(frame, 0, 0, eff.width, eff.height);
|
|
1048
|
+
}
|
|
1030
1049
|
function renderRef(ctx, eff, opts, _parentDoc) {
|
|
1031
1050
|
const visual = eff.visual;
|
|
1032
1051
|
const resolver = opts?.documentResolver;
|
|
@@ -1124,6 +1143,7 @@ function renderPlaceholder(ctx, eff, label) {
|
|
|
1124
1143
|
function renderFrame(ctx, resolvedFrame, doc, optsOrCache) {
|
|
1125
1144
|
let imageCache;
|
|
1126
1145
|
let documentResolver;
|
|
1146
|
+
let videoFrameProvider;
|
|
1127
1147
|
let maxRefDepth = 4;
|
|
1128
1148
|
if (optsOrCache && typeof optsOrCache.get === "function") {
|
|
1129
1149
|
imageCache = optsOrCache;
|
|
@@ -1131,6 +1151,7 @@ function renderFrame(ctx, resolvedFrame, doc, optsOrCache) {
|
|
|
1131
1151
|
const opts = optsOrCache;
|
|
1132
1152
|
imageCache = opts.imageCache;
|
|
1133
1153
|
documentResolver = opts.documentResolver;
|
|
1154
|
+
videoFrameProvider = opts.videoFrameProvider;
|
|
1134
1155
|
maxRefDepth = opts.maxRefDepth ?? 4;
|
|
1135
1156
|
}
|
|
1136
1157
|
const { width, height } = doc.canvas;
|
|
@@ -1138,10 +1159,14 @@ function renderFrame(ctx, resolvedFrame, doc, optsOrCache) {
|
|
|
1138
1159
|
ctx.fillRect(0, 0, width, height);
|
|
1139
1160
|
const effMap = /* @__PURE__ */ new Map();
|
|
1140
1161
|
const effList = [];
|
|
1162
|
+
const videoSourceTimeMap = /* @__PURE__ */ new Map();
|
|
1141
1163
|
for (const resolvedLayer of resolvedFrame.layers) {
|
|
1142
1164
|
const eff = buildEffectiveLayer(resolvedLayer, width, height);
|
|
1143
1165
|
effMap.set(resolvedLayer.layer.id, eff);
|
|
1144
1166
|
effList.push(eff);
|
|
1167
|
+
if (resolvedLayer.videoSourceTime !== void 0) {
|
|
1168
|
+
videoSourceTimeMap.set(resolvedLayer.layer.id, resolvedLayer.videoSourceTime);
|
|
1169
|
+
}
|
|
1145
1170
|
}
|
|
1146
1171
|
for (const eff of effList) {
|
|
1147
1172
|
const { layer } = eff;
|
|
@@ -1153,6 +1178,12 @@ function renderFrame(ctx, resolvedFrame, doc, optsOrCache) {
|
|
|
1153
1178
|
iv.src = doc.assets[iv.assetId].src;
|
|
1154
1179
|
}
|
|
1155
1180
|
}
|
|
1181
|
+
if (layer.visual.type === "video") {
|
|
1182
|
+
const vv = eff.visual;
|
|
1183
|
+
if (!vv.src && vv.assetId && doc.assets?.[vv.assetId]) {
|
|
1184
|
+
vv.src = doc.assets[vv.assetId].src;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1156
1187
|
ctx.save();
|
|
1157
1188
|
applyAncestorTransforms(ctx, layer.id, effMap, doc);
|
|
1158
1189
|
ctx.globalAlpha = eff.opacity;
|
|
@@ -1200,6 +1231,11 @@ function renderFrame(ctx, resolvedFrame, doc, optsOrCache) {
|
|
|
1200
1231
|
case "image":
|
|
1201
1232
|
if (imageCache) renderImage(renderCtx, eff, imageCache);
|
|
1202
1233
|
break;
|
|
1234
|
+
case "video":
|
|
1235
|
+
if (videoFrameProvider) {
|
|
1236
|
+
renderVideo(renderCtx, eff, videoSourceTimeMap.get(layer.id) ?? 0, videoFrameProvider);
|
|
1237
|
+
}
|
|
1238
|
+
break;
|
|
1203
1239
|
case "group":
|
|
1204
1240
|
break;
|
|
1205
1241
|
case "ref":
|
|
@@ -1225,6 +1261,9 @@ function renderFrame(ctx, resolvedFrame, doc, optsOrCache) {
|
|
|
1225
1261
|
case "image":
|
|
1226
1262
|
if (imageCache) renderImage(offCtx, eff, imageCache);
|
|
1227
1263
|
break;
|
|
1264
|
+
case "video":
|
|
1265
|
+
if (videoFrameProvider) renderVideo(offCtx, eff, videoSourceTimeMap.get(layer.id) ?? 0, videoFrameProvider);
|
|
1266
|
+
break;
|
|
1228
1267
|
case "ref":
|
|
1229
1268
|
renderRef(offCtx, eff, refOpts, doc);
|
|
1230
1269
|
break;
|
|
@@ -1240,6 +1279,9 @@ function renderFrame(ctx, resolvedFrame, doc, optsOrCache) {
|
|
|
1240
1279
|
case "image":
|
|
1241
1280
|
if (imageCache) renderImage(ctx, eff, imageCache);
|
|
1242
1281
|
break;
|
|
1282
|
+
case "video":
|
|
1283
|
+
if (videoFrameProvider) renderVideo(ctx, eff, videoSourceTimeMap.get(layer.id) ?? 0, videoFrameProvider);
|
|
1284
|
+
break;
|
|
1243
1285
|
case "ref":
|
|
1244
1286
|
renderRef(ctx, eff, refOpts, doc);
|
|
1245
1287
|
break;
|
|
@@ -1401,6 +1443,7 @@ var import_zod11 = require("zod");
|
|
|
1401
1443
|
var import_zod12 = require("zod");
|
|
1402
1444
|
var import_zod13 = require("zod");
|
|
1403
1445
|
var import_zod14 = require("zod");
|
|
1446
|
+
var import_zod15 = require("zod");
|
|
1404
1447
|
var import_yaml = require("yaml");
|
|
1405
1448
|
var PixelSchema = import_zod.z.number();
|
|
1406
1449
|
var PercentageSchema = import_zod.z.string().regex(/^-?\d+(\.\d+)?%$/, {
|
|
@@ -1642,6 +1685,18 @@ var ImageVisualSchema = import_zod7.z.object({
|
|
|
1642
1685
|
spritesheet: SpritesheetConfigSchema.optional(),
|
|
1643
1686
|
frameIndex: import_zod7.z.number().int().min(0).optional()
|
|
1644
1687
|
});
|
|
1688
|
+
var VideoVisualSchema = import_zod7.z.object({
|
|
1689
|
+
type: import_zod7.z.literal("video"),
|
|
1690
|
+
assetId: import_zod7.z.string().min(1, "assetId is required"),
|
|
1691
|
+
src: import_zod7.z.string().optional(),
|
|
1692
|
+
startFrame: import_zod7.z.number().int().min(0).optional(),
|
|
1693
|
+
sourceOffset: import_zod7.z.number().min(0).optional(),
|
|
1694
|
+
sourceEnd: import_zod7.z.number().positive().optional(),
|
|
1695
|
+
playbackRate: import_zod7.z.number().positive().optional(),
|
|
1696
|
+
volume: import_zod7.z.number().min(0).max(1).optional(),
|
|
1697
|
+
muted: import_zod7.z.boolean().optional(),
|
|
1698
|
+
objectFit: import_zod7.z.enum(["contain", "cover", "fill"]).optional()
|
|
1699
|
+
});
|
|
1645
1700
|
var GroupVisualSchema = import_zod7.z.object({
|
|
1646
1701
|
type: import_zod7.z.literal("group")
|
|
1647
1702
|
});
|
|
@@ -1655,6 +1710,7 @@ var VisualSchema = import_zod7.z.discriminatedUnion("type", [
|
|
|
1655
1710
|
ShapeVisualSchema,
|
|
1656
1711
|
TextVisualSchema,
|
|
1657
1712
|
ImageVisualSchema,
|
|
1713
|
+
VideoVisualSchema,
|
|
1658
1714
|
GroupVisualSchema,
|
|
1659
1715
|
RefVisualSchema
|
|
1660
1716
|
]);
|
|
@@ -1774,7 +1830,7 @@ var VariableSchema = import_zod12.z.object({
|
|
|
1774
1830
|
default: import_zod12.z.unknown().optional(),
|
|
1775
1831
|
description: import_zod12.z.string().optional()
|
|
1776
1832
|
});
|
|
1777
|
-
var AssetTypeSchema = import_zod13.z.enum(["image", "svg", "font", "animation", "audio"]);
|
|
1833
|
+
var AssetTypeSchema = import_zod13.z.enum(["image", "svg", "font", "animation", "audio", "video"]);
|
|
1778
1834
|
var AssetSchema = import_zod13.z.object({
|
|
1779
1835
|
type: AssetTypeSchema,
|
|
1780
1836
|
src: import_zod13.z.string().min(1, "Asset src is required"),
|
|
@@ -1785,6 +1841,12 @@ var AssetSchema = import_zod13.z.object({
|
|
|
1785
1841
|
frameCount: import_zod13.z.number().int().positive().optional(),
|
|
1786
1842
|
frameWidth: import_zod13.z.number().positive(),
|
|
1787
1843
|
frameHeight: import_zod13.z.number().positive()
|
|
1844
|
+
}).optional(),
|
|
1845
|
+
videoMeta: import_zod13.z.object({
|
|
1846
|
+
duration: import_zod13.z.number().positive("videoMeta.duration must be positive"),
|
|
1847
|
+
fps: import_zod13.z.number().positive("videoMeta.fps must be positive"),
|
|
1848
|
+
width: import_zod13.z.number().int().positive(),
|
|
1849
|
+
height: import_zod13.z.number().int().positive()
|
|
1788
1850
|
}).optional()
|
|
1789
1851
|
});
|
|
1790
1852
|
var CanvasSchema = import_zod14.z.object({
|
|
@@ -1805,6 +1867,121 @@ var AtelierDocumentSchema = import_zod14.z.object({
|
|
|
1805
1867
|
layers: import_zod14.z.array(LayerSchema),
|
|
1806
1868
|
states: import_zod14.z.record(import_zod14.z.string(), StateSchema)
|
|
1807
1869
|
});
|
|
1870
|
+
var SilencePolicySchema = import_zod15.z.object({
|
|
1871
|
+
noise: import_zod15.z.string().optional(),
|
|
1872
|
+
min_silence: import_zod15.z.number().nonnegative().optional(),
|
|
1873
|
+
default_padding_pre: import_zod15.z.number().nonnegative().optional(),
|
|
1874
|
+
default_padding_post: import_zod15.z.number().nonnegative().optional(),
|
|
1875
|
+
match_tolerance: import_zod15.z.number().nonnegative().optional()
|
|
1876
|
+
}).strict();
|
|
1877
|
+
var CaptionStyleSchema = import_zod15.z.object({
|
|
1878
|
+
font_family: import_zod15.z.string().optional(),
|
|
1879
|
+
font_size: import_zod15.z.number().positive().optional(),
|
|
1880
|
+
font_weight: import_zod15.z.union([import_zod15.z.literal("normal"), import_zod15.z.literal("bold"), import_zod15.z.number()]).optional(),
|
|
1881
|
+
text_align: import_zod15.z.enum(["left", "center", "right"]).optional(),
|
|
1882
|
+
color: import_zod15.z.string().optional(),
|
|
1883
|
+
y_ratio: import_zod15.z.number().min(0).max(1).optional(),
|
|
1884
|
+
width_ratio: import_zod15.z.number().min(0).max(1).optional(),
|
|
1885
|
+
fade_seconds: import_zod15.z.number().nonnegative().optional()
|
|
1886
|
+
}).strict();
|
|
1887
|
+
var CaptionGroupingSchema = import_zod15.z.object({
|
|
1888
|
+
max_words: import_zod15.z.number().int().positive().optional(),
|
|
1889
|
+
pause_gap: import_zod15.z.number().nonnegative().optional()
|
|
1890
|
+
}).strict();
|
|
1891
|
+
var OverlayAnchorSchema = import_zod15.z.enum([
|
|
1892
|
+
"top-left",
|
|
1893
|
+
"top-right",
|
|
1894
|
+
"bottom-left",
|
|
1895
|
+
"bottom-right"
|
|
1896
|
+
]);
|
|
1897
|
+
var OverlayTextStyleSchema = import_zod15.z.object({
|
|
1898
|
+
font_family: import_zod15.z.string().optional(),
|
|
1899
|
+
font_size: import_zod15.z.number().positive().optional(),
|
|
1900
|
+
font_weight: import_zod15.z.union([import_zod15.z.literal("normal"), import_zod15.z.literal("bold"), import_zod15.z.number()]).optional(),
|
|
1901
|
+
color: import_zod15.z.string().optional()
|
|
1902
|
+
}).strict();
|
|
1903
|
+
function validatePageNumberFormat(format, ctx) {
|
|
1904
|
+
if (format.length === 0) {
|
|
1905
|
+
ctx.addIssue({
|
|
1906
|
+
code: import_zod15.z.ZodIssueCode.custom,
|
|
1907
|
+
message: "format must be a non-empty string"
|
|
1908
|
+
});
|
|
1909
|
+
return;
|
|
1910
|
+
}
|
|
1911
|
+
const open = (format.match(/\{/g) ?? []).length;
|
|
1912
|
+
const close = (format.match(/\}/g) ?? []).length;
|
|
1913
|
+
if (open !== close) {
|
|
1914
|
+
ctx.addIssue({
|
|
1915
|
+
code: import_zod15.z.ZodIssueCode.custom,
|
|
1916
|
+
message: `format has unbalanced braces (${open} '{' vs ${close} '}')`
|
|
1917
|
+
});
|
|
1918
|
+
return;
|
|
1919
|
+
}
|
|
1920
|
+
const groupRe = /\{([^{}]*)\}/g;
|
|
1921
|
+
const groupRule = /^(current|total)(:0\d+d)?$/;
|
|
1922
|
+
let m;
|
|
1923
|
+
let sawCurrent = false;
|
|
1924
|
+
let sawTotal = false;
|
|
1925
|
+
while ((m = groupRe.exec(format)) !== null) {
|
|
1926
|
+
const inner = m[1];
|
|
1927
|
+
if (!groupRule.test(inner)) {
|
|
1928
|
+
ctx.addIssue({
|
|
1929
|
+
code: import_zod15.z.ZodIssueCode.custom,
|
|
1930
|
+
message: `format placeholder "{${inner}}" is not recognized \u2014 expected {current}, {total}, {current:0Nd}, or {total:0Nd}`
|
|
1931
|
+
});
|
|
1932
|
+
return;
|
|
1933
|
+
}
|
|
1934
|
+
if (inner.startsWith("current")) sawCurrent = true;
|
|
1935
|
+
if (inner.startsWith("total")) sawTotal = true;
|
|
1936
|
+
}
|
|
1937
|
+
if (!sawCurrent && !sawTotal) {
|
|
1938
|
+
ctx.addIssue({
|
|
1939
|
+
code: import_zod15.z.ZodIssueCode.custom,
|
|
1940
|
+
message: "format must contain at least one {current} or {total} placeholder"
|
|
1941
|
+
});
|
|
1942
|
+
}
|
|
1943
|
+
}
|
|
1944
|
+
var OverlayHandleRuleSchema = import_zod15.z.object({
|
|
1945
|
+
text: import_zod15.z.string().min(1),
|
|
1946
|
+
anchor: OverlayAnchorSchema,
|
|
1947
|
+
margin: import_zod15.z.number().nonnegative().optional(),
|
|
1948
|
+
style: OverlayTextStyleSchema.optional()
|
|
1949
|
+
}).strict();
|
|
1950
|
+
var OverlayPageNumberRuleSchema = import_zod15.z.object({
|
|
1951
|
+
format: import_zod15.z.string().superRefine(validatePageNumberFormat),
|
|
1952
|
+
anchor: OverlayAnchorSchema,
|
|
1953
|
+
margin: import_zod15.z.number().nonnegative().optional(),
|
|
1954
|
+
style: OverlayTextStyleSchema.optional()
|
|
1955
|
+
}).strict();
|
|
1956
|
+
var OverlayRulesSchema = import_zod15.z.object({
|
|
1957
|
+
handle: OverlayHandleRuleSchema.optional(),
|
|
1958
|
+
page_number: OverlayPageNumberRuleSchema.optional()
|
|
1959
|
+
}).strict();
|
|
1960
|
+
var StudioRecipeSchema = import_zod15.z.object({
|
|
1961
|
+
version: import_zod15.z.string(),
|
|
1962
|
+
name: import_zod15.z.string(),
|
|
1963
|
+
description: import_zod15.z.string().optional(),
|
|
1964
|
+
author: import_zod15.z.string().optional(),
|
|
1965
|
+
tags: import_zod15.z.array(import_zod15.z.string()).optional(),
|
|
1966
|
+
silence_policy: SilencePolicySchema.optional(),
|
|
1967
|
+
caption_style: CaptionStyleSchema.optional(),
|
|
1968
|
+
caption_grouping: CaptionGroupingSchema.optional(),
|
|
1969
|
+
// Phase 1.5 — first-class overlay rules
|
|
1970
|
+
overlay_rules: OverlayRulesSchema.optional(),
|
|
1971
|
+
// Reserved — Phase 3 (parse-opaque)
|
|
1972
|
+
caption_highlight: import_zod15.z.unknown().optional(),
|
|
1973
|
+
transition_kit: import_zod15.z.unknown().optional(),
|
|
1974
|
+
palette: import_zod15.z.unknown().optional(),
|
|
1975
|
+
audio_policy: import_zod15.z.unknown().optional(),
|
|
1976
|
+
aspect_targets: import_zod15.z.array(import_zod15.z.unknown()).optional()
|
|
1977
|
+
}).strict();
|
|
1978
|
+
var RESERVED_RECIPE_FIELDS = [
|
|
1979
|
+
"caption_highlight",
|
|
1980
|
+
"transition_kit",
|
|
1981
|
+
"palette",
|
|
1982
|
+
"audio_policy",
|
|
1983
|
+
"aspect_targets"
|
|
1984
|
+
];
|
|
1808
1985
|
function formatErrors(error) {
|
|
1809
1986
|
return error.issues.map((issue) => ({
|
|
1810
1987
|
path: issue.path.join(".") || "(root)",
|
|
@@ -1818,6 +1995,51 @@ function validateDocument(input) {
|
|
|
1818
1995
|
}
|
|
1819
1996
|
return { success: false, errors: formatErrors(result.error) };
|
|
1820
1997
|
}
|
|
1998
|
+
function validateVideoLayer(visual, videoMetaDuration) {
|
|
1999
|
+
const errors = [];
|
|
2000
|
+
if (!visual.assetId) {
|
|
2001
|
+
errors.push({ path: "assetId", message: "assetId is required" });
|
|
2002
|
+
}
|
|
2003
|
+
const sourceOffset = visual.sourceOffset ?? 0;
|
|
2004
|
+
if (visual.sourceEnd !== void 0) {
|
|
2005
|
+
if (visual.sourceEnd <= sourceOffset) {
|
|
2006
|
+
errors.push({
|
|
2007
|
+
path: "sourceEnd",
|
|
2008
|
+
message: `sourceEnd (${visual.sourceEnd}) must be greater than sourceOffset (${sourceOffset})`
|
|
2009
|
+
});
|
|
2010
|
+
}
|
|
2011
|
+
if (videoMetaDuration !== void 0 && visual.sourceEnd > videoMetaDuration) {
|
|
2012
|
+
errors.push({
|
|
2013
|
+
path: "sourceEnd",
|
|
2014
|
+
message: `sourceEnd (${visual.sourceEnd}) exceeds asset duration (${videoMetaDuration})`
|
|
2015
|
+
});
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
if (videoMetaDuration !== void 0 && sourceOffset >= videoMetaDuration) {
|
|
2019
|
+
errors.push({
|
|
2020
|
+
path: "sourceOffset",
|
|
2021
|
+
message: `sourceOffset (${sourceOffset}) is at or beyond asset duration (${videoMetaDuration})`
|
|
2022
|
+
});
|
|
2023
|
+
}
|
|
2024
|
+
if (errors.length > 0) return { success: false, errors };
|
|
2025
|
+
return { success: true, data: visual };
|
|
2026
|
+
}
|
|
2027
|
+
function validateRecipe(recipe) {
|
|
2028
|
+
const parsed = StudioRecipeSchema.safeParse(recipe);
|
|
2029
|
+
if (!parsed.success) {
|
|
2030
|
+
return { success: false, errors: formatErrors(parsed.error) };
|
|
2031
|
+
}
|
|
2032
|
+
const warnings = [];
|
|
2033
|
+
const data = parsed.data;
|
|
2034
|
+
for (const field of RESERVED_RECIPE_FIELDS) {
|
|
2035
|
+
if (data[field] !== void 0) {
|
|
2036
|
+
warnings.push(
|
|
2037
|
+
`${field} is reserved for Phase 3 and currently has no effect.`
|
|
2038
|
+
);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
return { success: true, data, ...warnings.length > 0 && { warnings } };
|
|
2042
|
+
}
|
|
1821
2043
|
function parseAtelier(yamlString) {
|
|
1822
2044
|
let parsed;
|
|
1823
2045
|
try {
|
|
@@ -1830,6 +2052,9 @@ function parseAtelier(yamlString) {
|
|
|
1830
2052
|
}
|
|
1831
2053
|
return validateDocument(parsed);
|
|
1832
2054
|
}
|
|
2055
|
+
function serializeAtelier(doc) {
|
|
2056
|
+
return (0, import_yaml.stringify)(doc, { indent: 2 });
|
|
2057
|
+
}
|
|
1833
2058
|
|
|
1834
2059
|
// src/commands/validate.ts
|
|
1835
2060
|
init_dist2();
|
|
@@ -1857,29 +2082,1938 @@ function validateFile(filePath) {
|
|
|
1857
2082
|
overlapErrors.push(`State "${stateName}": ${overlap.message}`);
|
|
1858
2083
|
}
|
|
1859
2084
|
}
|
|
1860
|
-
if (overlapErrors.length > 0) {
|
|
1861
|
-
return { valid: false, errors: overlapErrors };
|
|
2085
|
+
if (overlapErrors.length > 0) {
|
|
2086
|
+
return { valid: false, errors: overlapErrors };
|
|
2087
|
+
}
|
|
2088
|
+
return { valid: true, errors: [] };
|
|
2089
|
+
}
|
|
2090
|
+
function validateCommand(program2) {
|
|
2091
|
+
program2.command("validate <file>").description("Validate an .atelier YAML file").action((file) => {
|
|
2092
|
+
const { valid, errors } = validateFile(file);
|
|
2093
|
+
if (valid) {
|
|
2094
|
+
console.log("Valid");
|
|
2095
|
+
} else {
|
|
2096
|
+
console.error("Validation errors:");
|
|
2097
|
+
for (const error of errors) {
|
|
2098
|
+
console.error(` - ${error}`);
|
|
2099
|
+
}
|
|
2100
|
+
process.exit(1);
|
|
2101
|
+
}
|
|
2102
|
+
});
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
// src/commands/lint.ts
|
|
2106
|
+
var import_node_fs2 = require("fs");
|
|
2107
|
+
var import_node_path2 = require("path");
|
|
2108
|
+
init_dist2();
|
|
2109
|
+
function lintFile(filePath) {
|
|
2110
|
+
const absPath = (0, import_node_path2.resolve)(filePath);
|
|
2111
|
+
let content;
|
|
2112
|
+
try {
|
|
2113
|
+
content = (0, import_node_fs2.readFileSync)(absPath, "utf-8");
|
|
2114
|
+
} catch {
|
|
2115
|
+
return {
|
|
2116
|
+
file: absPath,
|
|
2117
|
+
valid: false,
|
|
2118
|
+
gates: [
|
|
2119
|
+
{
|
|
2120
|
+
gate: "^valid-document",
|
|
2121
|
+
pass: false,
|
|
2122
|
+
errors: [`Cannot read file: ${absPath}`]
|
|
2123
|
+
}
|
|
2124
|
+
]
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
const gates = [];
|
|
2128
|
+
const parseResult = parseAtelier(content);
|
|
2129
|
+
if (!parseResult.success) {
|
|
2130
|
+
gates.push({
|
|
2131
|
+
gate: "^valid-document",
|
|
2132
|
+
pass: false,
|
|
2133
|
+
errors: parseResult.errors.map((e) => `${e.path}: ${e.message}`)
|
|
2134
|
+
});
|
|
2135
|
+
return { file: absPath, valid: false, gates };
|
|
2136
|
+
}
|
|
2137
|
+
gates.push({ gate: "^valid-document", pass: true, errors: [] });
|
|
2138
|
+
const doc = parseResult.data;
|
|
2139
|
+
const deltaErrors = [];
|
|
2140
|
+
for (const [stateName, state] of Object.entries(doc.states)) {
|
|
2141
|
+
const overlaps = validateAllDeltas(state.deltas);
|
|
2142
|
+
for (const overlap of overlaps) {
|
|
2143
|
+
deltaErrors.push(`State "${stateName}": ${overlap.message}`);
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
2146
|
+
gates.push({
|
|
2147
|
+
gate: "^valid-delta",
|
|
2148
|
+
pass: deltaErrors.length === 0,
|
|
2149
|
+
errors: deltaErrors
|
|
2150
|
+
});
|
|
2151
|
+
const videoErrors = [];
|
|
2152
|
+
for (const layer of doc.layers) {
|
|
2153
|
+
if (layer.visual.type !== "video") continue;
|
|
2154
|
+
const visual = layer.visual;
|
|
2155
|
+
const duration = doc.assets?.[visual.assetId]?.videoMeta?.duration;
|
|
2156
|
+
const result = validateVideoLayer(visual, duration);
|
|
2157
|
+
if (!result.success) {
|
|
2158
|
+
for (const err of result.errors) {
|
|
2159
|
+
videoErrors.push(`Layer "${layer.id}" (${err.path}): ${err.message}`);
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
gates.push({
|
|
2164
|
+
gate: "^valid-video-layer",
|
|
2165
|
+
pass: videoErrors.length === 0,
|
|
2166
|
+
errors: videoErrors
|
|
2167
|
+
});
|
|
2168
|
+
const valid = gates.every((g) => g.pass);
|
|
2169
|
+
return { file: absPath, valid, gates };
|
|
2170
|
+
}
|
|
2171
|
+
function formatResult(result) {
|
|
2172
|
+
const lines = [];
|
|
2173
|
+
const status = result.valid ? "PASS" : "FAIL";
|
|
2174
|
+
lines.push(`${status} ${result.file}`);
|
|
2175
|
+
for (const gate of result.gates) {
|
|
2176
|
+
const gateStatus = gate.pass ? " \u2713" : " \u2717";
|
|
2177
|
+
lines.push(`${gateStatus} ${gate.gate}`);
|
|
2178
|
+
for (const err of gate.errors) {
|
|
2179
|
+
lines.push(` ${err}`);
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
return lines.join("\n");
|
|
2183
|
+
}
|
|
2184
|
+
function lintCommand(program2) {
|
|
2185
|
+
program2.command("lint <files...>").description(
|
|
2186
|
+
"Lint .atelier files against all gates (^valid-document, ^valid-delta, ^valid-video-layer)"
|
|
2187
|
+
).option("--json", "Output results as JSON array").action((files, opts) => {
|
|
2188
|
+
const results = files.map(lintFile);
|
|
2189
|
+
if (opts.json) {
|
|
2190
|
+
console.log(JSON.stringify(results, null, 2));
|
|
2191
|
+
} else {
|
|
2192
|
+
for (const result of results) {
|
|
2193
|
+
console.log(formatResult(result));
|
|
2194
|
+
}
|
|
2195
|
+
}
|
|
2196
|
+
const allValid = results.every((r) => r.valid);
|
|
2197
|
+
if (!allValid) process.exit(1);
|
|
2198
|
+
});
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
// src/lib/video-project.ts
|
|
2202
|
+
var import_node_fs3 = require("fs");
|
|
2203
|
+
var import_node_path3 = require("path");
|
|
2204
|
+
var VIDEO_PROJECT_VERSION = "1.0";
|
|
2205
|
+
var VIDEO_CUTLIST_VERSION = "1.1";
|
|
2206
|
+
var VIDEO_TRANSCRIPT_VERSION = "1.1";
|
|
2207
|
+
function effectiveSpan(cut, duration) {
|
|
2208
|
+
return {
|
|
2209
|
+
start: Math.max(0, cut.rawStart - cut.paddingPre),
|
|
2210
|
+
end: Math.min(duration, cut.rawEnd + cut.paddingPost)
|
|
2211
|
+
};
|
|
2212
|
+
}
|
|
2213
|
+
function loadVideoProject(dir) {
|
|
2214
|
+
const projectDir = (0, import_node_path3.resolve)(dir);
|
|
2215
|
+
const possibleExts = [".mp4", ".mov", ".webm", ".mkv", ".avi"];
|
|
2216
|
+
let sourceFilename = "source.mp4";
|
|
2217
|
+
for (const ext of possibleExts) {
|
|
2218
|
+
if ((0, import_node_fs3.existsSync)((0, import_node_path3.join)(projectDir, `source${ext}`))) {
|
|
2219
|
+
sourceFilename = `source${ext}`;
|
|
2220
|
+
break;
|
|
2221
|
+
}
|
|
2222
|
+
}
|
|
2223
|
+
const manifest = {
|
|
2224
|
+
source: sourceFilename,
|
|
2225
|
+
composition: "project.atelier",
|
|
2226
|
+
transcript: "transcript.json",
|
|
2227
|
+
cuts: "cuts.json",
|
|
2228
|
+
exportDir: "export/",
|
|
2229
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2230
|
+
version: VIDEO_PROJECT_VERSION
|
|
2231
|
+
};
|
|
2232
|
+
return {
|
|
2233
|
+
dir: projectDir,
|
|
2234
|
+
sourcePath: (0, import_node_path3.join)(projectDir, sourceFilename),
|
|
2235
|
+
compositionPath: (0, import_node_path3.join)(projectDir, "project.atelier"),
|
|
2236
|
+
transcriptPath: (0, import_node_path3.join)(projectDir, "transcript.json"),
|
|
2237
|
+
cutsPath: (0, import_node_path3.join)(projectDir, "cuts.json"),
|
|
2238
|
+
exportDir: (0, import_node_path3.join)(projectDir, "export"),
|
|
2239
|
+
manifest
|
|
2240
|
+
};
|
|
2241
|
+
}
|
|
2242
|
+
function readCutList(project) {
|
|
2243
|
+
if (!(0, import_node_fs3.existsSync)(project.cutsPath)) {
|
|
2244
|
+
return { version: VIDEO_CUTLIST_VERSION, source: project.manifest.source, cuts: [] };
|
|
2245
|
+
}
|
|
2246
|
+
const raw = JSON.parse((0, import_node_fs3.readFileSync)(project.cutsPath, "utf-8"));
|
|
2247
|
+
const cuts = raw.cuts.map((entry) => {
|
|
2248
|
+
if ("rawStart" in entry) return entry;
|
|
2249
|
+
return {
|
|
2250
|
+
rawStart: entry.start,
|
|
2251
|
+
rawEnd: entry.end,
|
|
2252
|
+
paddingPre: 0,
|
|
2253
|
+
paddingPost: 0,
|
|
2254
|
+
...entry.label !== void 0 && { label: entry.label }
|
|
2255
|
+
};
|
|
2256
|
+
});
|
|
2257
|
+
return {
|
|
2258
|
+
version: VIDEO_CUTLIST_VERSION,
|
|
2259
|
+
source: raw.source,
|
|
2260
|
+
cuts
|
|
2261
|
+
};
|
|
2262
|
+
}
|
|
2263
|
+
function writeCutList(project, cuts) {
|
|
2264
|
+
const payload = { ...cuts, version: VIDEO_CUTLIST_VERSION };
|
|
2265
|
+
(0, import_node_fs3.writeFileSync)(project.cutsPath, JSON.stringify(payload, null, 2), "utf-8");
|
|
2266
|
+
}
|
|
2267
|
+
function readTranscript(project) {
|
|
2268
|
+
if (!(0, import_node_fs3.existsSync)(project.transcriptPath)) return null;
|
|
2269
|
+
const raw = JSON.parse((0, import_node_fs3.readFileSync)(project.transcriptPath, "utf-8"));
|
|
2270
|
+
const segments = raw.segments.map((seg) => ({
|
|
2271
|
+
text: seg.text,
|
|
2272
|
+
start: seg.start,
|
|
2273
|
+
end: seg.end,
|
|
2274
|
+
words: seg.words.map((w) => {
|
|
2275
|
+
if ("detected" in w) return w;
|
|
2276
|
+
return {
|
|
2277
|
+
detected: w.word,
|
|
2278
|
+
text: w.word,
|
|
2279
|
+
start: w.start,
|
|
2280
|
+
end: w.end,
|
|
2281
|
+
...w.confidence !== void 0 && { confidence: w.confidence }
|
|
2282
|
+
};
|
|
2283
|
+
})
|
|
2284
|
+
}));
|
|
2285
|
+
return {
|
|
2286
|
+
version: VIDEO_TRANSCRIPT_VERSION,
|
|
2287
|
+
...raw.language !== void 0 && { language: raw.language },
|
|
2288
|
+
segments
|
|
2289
|
+
};
|
|
2290
|
+
}
|
|
2291
|
+
function writeTranscript(project, transcript) {
|
|
2292
|
+
const payload = { ...transcript, version: VIDEO_TRANSCRIPT_VERSION };
|
|
2293
|
+
(0, import_node_fs3.writeFileSync)(project.transcriptPath, JSON.stringify(payload, null, 2), "utf-8");
|
|
2294
|
+
}
|
|
2295
|
+
function readComposition(project) {
|
|
2296
|
+
return JSON.parse((0, import_node_fs3.readFileSync)(project.compositionPath, "utf-8"));
|
|
2297
|
+
}
|
|
2298
|
+
function writeComposition(project, doc) {
|
|
2299
|
+
(0, import_node_fs3.writeFileSync)(project.compositionPath, JSON.stringify(doc, null, 2), "utf-8");
|
|
2300
|
+
}
|
|
2301
|
+
function rewriteCutLayers(doc, cuts, sourceFilename, sourceDuration, assetId = "src") {
|
|
2302
|
+
const preserved = doc.layers.filter((l) => !(l.tags ?? []).includes("silence-trim"));
|
|
2303
|
+
const fps = doc.canvas.fps;
|
|
2304
|
+
let cumulativeFrame = 0;
|
|
2305
|
+
const trimLayers = cuts.map((cut, idx) => {
|
|
2306
|
+
const span = effectiveSpan(cut, sourceDuration);
|
|
2307
|
+
const sourceOffsetFrames = Math.floor(span.start * fps) / fps;
|
|
2308
|
+
const sourceEndFrames = Math.ceil(span.end * fps) / fps;
|
|
2309
|
+
const durationFrames = Math.max(1, Math.round((sourceEndFrames - sourceOffsetFrames) * fps));
|
|
2310
|
+
const layer = {
|
|
2311
|
+
id: `clip-trim-${idx}`,
|
|
2312
|
+
tags: ["silence-trim"],
|
|
2313
|
+
visual: {
|
|
2314
|
+
type: "video",
|
|
2315
|
+
assetId,
|
|
2316
|
+
src: sourceFilename,
|
|
2317
|
+
startFrame: cumulativeFrame,
|
|
2318
|
+
sourceOffset: sourceOffsetFrames,
|
|
2319
|
+
sourceEnd: sourceEndFrames,
|
|
2320
|
+
playbackRate: 1,
|
|
2321
|
+
objectFit: "contain"
|
|
2322
|
+
},
|
|
2323
|
+
frame: { x: 0, y: 0 },
|
|
2324
|
+
bounds: { width: doc.canvas.width, height: doc.canvas.height }
|
|
2325
|
+
};
|
|
2326
|
+
cumulativeFrame += durationFrames;
|
|
2327
|
+
return layer;
|
|
2328
|
+
});
|
|
2329
|
+
return { ...doc, layers: [...preserved, ...trimLayers] };
|
|
2330
|
+
}
|
|
2331
|
+
|
|
2332
|
+
// src/lib/silence-detect.ts
|
|
2333
|
+
var import_node_child_process = require("child_process");
|
|
2334
|
+
async function probeSilencedetect() {
|
|
2335
|
+
const stdout = await runCapture("ffmpeg", ["-hide_banner", "-filters"]);
|
|
2336
|
+
if (!/\bsilencedetect\b/.test(stdout)) {
|
|
2337
|
+
throw new Error(
|
|
2338
|
+
"Your ffmpeg build lacks the silencedetect filter. Install a standard ffmpeg \u2265 4.0 (brew install ffmpeg / apt install ffmpeg)."
|
|
2339
|
+
);
|
|
2340
|
+
}
|
|
2341
|
+
}
|
|
2342
|
+
async function probeDuration(sourcePath) {
|
|
2343
|
+
const stdout = await runCapture("ffprobe", [
|
|
2344
|
+
"-v",
|
|
2345
|
+
"error",
|
|
2346
|
+
"-show_entries",
|
|
2347
|
+
"format=duration",
|
|
2348
|
+
"-of",
|
|
2349
|
+
"csv=p=0",
|
|
2350
|
+
sourcePath
|
|
2351
|
+
]);
|
|
2352
|
+
const n = parseFloat(stdout.trim());
|
|
2353
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
2354
|
+
throw new Error(`ffprobe returned invalid duration for ${sourcePath}: "${stdout}"`);
|
|
2355
|
+
}
|
|
2356
|
+
return n;
|
|
2357
|
+
}
|
|
2358
|
+
async function runSilenceDetect(sourcePath, options = {}) {
|
|
2359
|
+
const noise = options.noise ?? "-30dB";
|
|
2360
|
+
const minSilence = options.minSilence ?? 0.35;
|
|
2361
|
+
const filter = `silencedetect=noise=${noise}:d=${minSilence}`;
|
|
2362
|
+
const stderr = await runCaptureStderr("ffmpeg", [
|
|
2363
|
+
"-hide_banner",
|
|
2364
|
+
"-nostats",
|
|
2365
|
+
"-i",
|
|
2366
|
+
sourcePath,
|
|
2367
|
+
"-af",
|
|
2368
|
+
filter,
|
|
2369
|
+
"-f",
|
|
2370
|
+
"null",
|
|
2371
|
+
"-"
|
|
2372
|
+
]);
|
|
2373
|
+
return parseSilenceDetectStderr(stderr);
|
|
2374
|
+
}
|
|
2375
|
+
function parseSilenceDetectStderr(stderr) {
|
|
2376
|
+
const intervals = [];
|
|
2377
|
+
const startRe = /silence_start:\s*(-?[\d.]+)/;
|
|
2378
|
+
const endRe = /silence_end:\s*(-?[\d.]+)/;
|
|
2379
|
+
let pendingStart = null;
|
|
2380
|
+
for (const line of stderr.split(/\r?\n/)) {
|
|
2381
|
+
const sm = line.match(startRe);
|
|
2382
|
+
if (sm) {
|
|
2383
|
+
pendingStart = parseFloat(sm[1]);
|
|
2384
|
+
continue;
|
|
2385
|
+
}
|
|
2386
|
+
const em = line.match(endRe);
|
|
2387
|
+
if (em && pendingStart !== null) {
|
|
2388
|
+
const end = parseFloat(em[1]);
|
|
2389
|
+
intervals.push({ start: Math.max(0, pendingStart), end });
|
|
2390
|
+
pendingStart = null;
|
|
2391
|
+
}
|
|
2392
|
+
}
|
|
2393
|
+
return intervals;
|
|
2394
|
+
}
|
|
2395
|
+
function runCapture(cmd, args) {
|
|
2396
|
+
return new Promise((resolve16, reject) => {
|
|
2397
|
+
const proc = (0, import_node_child_process.spawn)(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
2398
|
+
let stdout = "";
|
|
2399
|
+
proc.stdout.on("data", (b) => stdout += b.toString());
|
|
2400
|
+
proc.on("error", (err) => {
|
|
2401
|
+
const e = err;
|
|
2402
|
+
if (e.code === "ENOENT") {
|
|
2403
|
+
reject(new Error(`${cmd} not found on PATH. Install ffmpeg/ffprobe (brew install ffmpeg).`));
|
|
2404
|
+
} else {
|
|
2405
|
+
reject(err);
|
|
2406
|
+
}
|
|
2407
|
+
});
|
|
2408
|
+
proc.on("close", (code) => {
|
|
2409
|
+
if (code !== 0) reject(new Error(`${cmd} exited ${code}`));
|
|
2410
|
+
else resolve16(stdout);
|
|
2411
|
+
});
|
|
2412
|
+
});
|
|
2413
|
+
}
|
|
2414
|
+
function runCaptureStderr(cmd, args) {
|
|
2415
|
+
return new Promise((resolve16, reject) => {
|
|
2416
|
+
const proc = (0, import_node_child_process.spawn)(cmd, args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
2417
|
+
let stderr = "";
|
|
2418
|
+
proc.stderr.on("data", (b) => stderr += b.toString());
|
|
2419
|
+
proc.on("error", (err) => {
|
|
2420
|
+
const e = err;
|
|
2421
|
+
if (e.code === "ENOENT") {
|
|
2422
|
+
reject(new Error(`${cmd} not found on PATH. Install ffmpeg (brew install ffmpeg).`));
|
|
2423
|
+
} else {
|
|
2424
|
+
reject(err);
|
|
2425
|
+
}
|
|
2426
|
+
});
|
|
2427
|
+
proc.on("close", () => resolve16(stderr));
|
|
2428
|
+
});
|
|
2429
|
+
}
|
|
2430
|
+
|
|
2431
|
+
// src/lib/cut-model.ts
|
|
2432
|
+
var DEFAULT_PADDING_PRE = 0.08;
|
|
2433
|
+
var DEFAULT_PADDING_POST = 0.12;
|
|
2434
|
+
var DEFAULT_MATCH_TOLERANCE = 0.5;
|
|
2435
|
+
function invertToSpeechIntervals(silences, duration) {
|
|
2436
|
+
if (silences.length === 0) {
|
|
2437
|
+
return duration > 0 ? [{ start: 0, end: duration }] : [];
|
|
2438
|
+
}
|
|
2439
|
+
const sorted = [...silences].sort((a, b) => a.start - b.start);
|
|
2440
|
+
const speech = [];
|
|
2441
|
+
if (sorted[0].start > 0) {
|
|
2442
|
+
speech.push({ start: 0, end: sorted[0].start });
|
|
2443
|
+
}
|
|
2444
|
+
for (let i = 0; i < sorted.length - 1; i++) {
|
|
2445
|
+
const gapStart = sorted[i].end;
|
|
2446
|
+
const gapEnd = sorted[i + 1].start;
|
|
2447
|
+
if (gapEnd > gapStart) {
|
|
2448
|
+
speech.push({ start: gapStart, end: gapEnd });
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
const last = sorted[sorted.length - 1];
|
|
2452
|
+
if (last.end < duration) {
|
|
2453
|
+
speech.push({ start: last.end, end: duration });
|
|
2454
|
+
}
|
|
2455
|
+
return speech;
|
|
2456
|
+
}
|
|
2457
|
+
function buildInitialCuts(speech, paddingPre = DEFAULT_PADDING_PRE, paddingPost = DEFAULT_PADDING_POST) {
|
|
2458
|
+
return speech.map((s) => ({
|
|
2459
|
+
rawStart: s.start,
|
|
2460
|
+
rawEnd: s.end,
|
|
2461
|
+
paddingPre,
|
|
2462
|
+
paddingPost
|
|
2463
|
+
}));
|
|
2464
|
+
}
|
|
2465
|
+
function resolveOverlaps(cuts) {
|
|
2466
|
+
for (let i = 0; i < cuts.length - 1; i++) {
|
|
2467
|
+
const a = cuts[i];
|
|
2468
|
+
const b = cuts[i + 1];
|
|
2469
|
+
const aEnd = a.rawEnd + a.paddingPost;
|
|
2470
|
+
const bStart = b.rawStart - b.paddingPre;
|
|
2471
|
+
if (aEnd > bStart) {
|
|
2472
|
+
const mid = (a.rawEnd + b.rawStart) / 2;
|
|
2473
|
+
a.paddingPost = Math.max(0, mid - a.rawEnd);
|
|
2474
|
+
b.paddingPre = Math.max(0, b.rawStart - mid);
|
|
2475
|
+
}
|
|
2476
|
+
}
|
|
2477
|
+
}
|
|
2478
|
+
function clampBoundaries(cuts, duration) {
|
|
2479
|
+
if (cuts.length === 0) return;
|
|
2480
|
+
const first = cuts[0];
|
|
2481
|
+
if (first.rawStart - first.paddingPre < 0) {
|
|
2482
|
+
first.paddingPre = first.rawStart;
|
|
2483
|
+
}
|
|
2484
|
+
const last = cuts[cuts.length - 1];
|
|
2485
|
+
if (last.rawEnd + last.paddingPost > duration) {
|
|
2486
|
+
last.paddingPost = Math.max(0, duration - last.rawEnd);
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
function mergeWithExisting(fresh, existing, tolerance = DEFAULT_MATCH_TOLERANCE) {
|
|
2490
|
+
return fresh.map((f) => {
|
|
2491
|
+
const match = existing.find(
|
|
2492
|
+
(e) => Math.abs(e.rawStart - f.rawStart) < tolerance && Math.abs(e.rawEnd - f.rawEnd) < tolerance
|
|
2493
|
+
);
|
|
2494
|
+
if (!match) return f;
|
|
2495
|
+
return {
|
|
2496
|
+
rawStart: f.rawStart,
|
|
2497
|
+
rawEnd: f.rawEnd,
|
|
2498
|
+
paddingPre: match.paddingPre,
|
|
2499
|
+
paddingPost: match.paddingPost,
|
|
2500
|
+
...match.label !== void 0 && { label: match.label }
|
|
2501
|
+
};
|
|
2502
|
+
});
|
|
2503
|
+
}
|
|
2504
|
+
function applyGlobalPadding(cuts, deltaSeconds) {
|
|
2505
|
+
for (const cut of cuts) {
|
|
2506
|
+
cut.paddingPre = Math.max(0, cut.paddingPre + deltaSeconds);
|
|
2507
|
+
cut.paddingPost = Math.max(0, cut.paddingPost + deltaSeconds);
|
|
2508
|
+
}
|
|
2509
|
+
}
|
|
2510
|
+
|
|
2511
|
+
// src/lib/recipe.ts
|
|
2512
|
+
var import_node_fs4 = require("fs");
|
|
2513
|
+
var import_node_path4 = require("path");
|
|
2514
|
+
var import_node_os = require("os");
|
|
2515
|
+
var import_yaml2 = require("yaml");
|
|
2516
|
+
var RECIPE_VERSION = "1.0";
|
|
2517
|
+
function resolveRecipePath(pathOrName, projectDir) {
|
|
2518
|
+
if ((0, import_node_path4.isAbsolute)(pathOrName) || pathOrName.includes("/") || pathOrName.includes("\\")) {
|
|
2519
|
+
return (0, import_node_path4.resolve)(pathOrName);
|
|
2520
|
+
}
|
|
2521
|
+
const candidates = [];
|
|
2522
|
+
const exts = [".recipe.yaml", ".recipe.json", ".yaml", ".yml", ".json"];
|
|
2523
|
+
if (projectDir) {
|
|
2524
|
+
const projectRecipesDir = (0, import_node_path4.join)((0, import_node_path4.resolve)(projectDir), ".atelier", "recipes");
|
|
2525
|
+
for (const ext of exts) candidates.push((0, import_node_path4.join)(projectRecipesDir, `${pathOrName}${ext}`));
|
|
2526
|
+
}
|
|
2527
|
+
const userRecipesDir = (0, import_node_path4.join)((0, import_node_os.homedir)(), ".atelier", "recipes");
|
|
2528
|
+
for (const ext of exts) candidates.push((0, import_node_path4.join)(userRecipesDir, `${pathOrName}${ext}`));
|
|
2529
|
+
for (const candidate of candidates) {
|
|
2530
|
+
if ((0, import_node_fs4.existsSync)(candidate)) return candidate;
|
|
2531
|
+
}
|
|
2532
|
+
throw new Error(
|
|
2533
|
+
`Recipe "${pathOrName}" not found. Looked in:
|
|
2534
|
+
${candidates.map((c) => ` ${c}`).join("\n")}`
|
|
2535
|
+
);
|
|
2536
|
+
}
|
|
2537
|
+
function loadRecipe(pathOrName, projectDir) {
|
|
2538
|
+
const path = resolveRecipePath(pathOrName, projectDir);
|
|
2539
|
+
const raw = (0, import_node_fs4.readFileSync)(path, "utf-8");
|
|
2540
|
+
let parsed;
|
|
2541
|
+
if (path.endsWith(".json")) {
|
|
2542
|
+
parsed = JSON.parse(raw);
|
|
2543
|
+
} else {
|
|
2544
|
+
parsed = (0, import_yaml2.parse)(raw);
|
|
2545
|
+
}
|
|
2546
|
+
const result = validateRecipe(parsed);
|
|
2547
|
+
if (!result.success) {
|
|
2548
|
+
const msg = result.errors.map((e) => ` ${e.path}: ${e.message}`).join("\n");
|
|
2549
|
+
throw new Error(`Invalid recipe at ${path}:
|
|
2550
|
+
${msg}`);
|
|
2551
|
+
}
|
|
2552
|
+
return {
|
|
2553
|
+
recipe: result.data,
|
|
2554
|
+
path,
|
|
2555
|
+
warnings: result.warnings ?? []
|
|
2556
|
+
};
|
|
2557
|
+
}
|
|
2558
|
+
function scaffoldRecipeYaml(name) {
|
|
2559
|
+
return `# Studio Recipe \u2014 ${name}
|
|
2560
|
+
# Phase 1 \u2014 manual authoring + apply
|
|
2561
|
+
# https://github.com/ascend42/a-atelier/blob/main/.paradigm/specs/studio-recipe.md
|
|
2562
|
+
|
|
2563
|
+
version: "${RECIPE_VERSION}"
|
|
2564
|
+
name: "${name}"
|
|
2565
|
+
description: ""
|
|
2566
|
+
author: ""
|
|
2567
|
+
tags: []
|
|
2568
|
+
|
|
2569
|
+
# \u2500\u2500 Silence-trim policy \u2014 consumed by \`atelier trim\` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2570
|
+
silence_policy:
|
|
2571
|
+
# silencedetect noise threshold (default: -30dB)
|
|
2572
|
+
noise: "-30dB"
|
|
2573
|
+
# Minimum silence duration to register, in seconds (default: 0.35)
|
|
2574
|
+
min_silence: 0.35
|
|
2575
|
+
# Default leading padding for new cuts, in seconds (default: 0.08)
|
|
2576
|
+
default_padding_pre: 0.08
|
|
2577
|
+
# Default trailing padding for new cuts, in seconds (default: 0.12)
|
|
2578
|
+
default_padding_post: 0.12
|
|
2579
|
+
# Re-detect match tolerance for preserving user padding, in seconds (default: 0.5)
|
|
2580
|
+
match_tolerance: 0.5
|
|
2581
|
+
|
|
2582
|
+
# \u2500\u2500 Caption visual style \u2014 consumed by \`atelier transcribe\` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2583
|
+
caption_style:
|
|
2584
|
+
font_family: "Inter"
|
|
2585
|
+
font_size: 84
|
|
2586
|
+
font_weight: "bold" # normal | bold | numeric (100..900)
|
|
2587
|
+
text_align: "center" # left | center | right
|
|
2588
|
+
color: "#FFFFFF"
|
|
2589
|
+
y_ratio: 0.85 # 0=top, 1=bottom
|
|
2590
|
+
width_ratio: 0.9
|
|
2591
|
+
fade_seconds: 0.05
|
|
2592
|
+
|
|
2593
|
+
# \u2500\u2500 Caption phrase grouping \u2014 consumed by \`atelier transcribe\` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2594
|
+
caption_grouping:
|
|
2595
|
+
max_words: 5
|
|
2596
|
+
pause_gap: 0.4
|
|
2597
|
+
|
|
2598
|
+
# \u2500\u2500 Overlay rules (Phase 1.5) \u2014 anchored handle + page-number overlays \u2500
|
|
2599
|
+
# overlay_rules:
|
|
2600
|
+
# handle:
|
|
2601
|
+
# text: "@username"
|
|
2602
|
+
# anchor: "bottom-left" # top-left | top-right | bottom-left | bottom-right
|
|
2603
|
+
# margin: 24 # px from anchored edges
|
|
2604
|
+
# style:
|
|
2605
|
+
# font_family: "Inter"
|
|
2606
|
+
# font_size: 36
|
|
2607
|
+
# font_weight: "bold"
|
|
2608
|
+
# color: "#FFFFFF"
|
|
2609
|
+
# page_number:
|
|
2610
|
+
# format: "{current}/{total}" # supports {current:02d} / {total:02d} zero-pad
|
|
2611
|
+
# anchor: "top-right"
|
|
2612
|
+
# margin: 24
|
|
2613
|
+
# style:
|
|
2614
|
+
# font_family: "Inter"
|
|
2615
|
+
# font_size: 36
|
|
2616
|
+
# font_weight: "normal"
|
|
2617
|
+
# color: "#FFFFFF"
|
|
2618
|
+
|
|
2619
|
+
# \u2500\u2500 Phase 3 fields (reserved \u2014 Phase 1 ignores these) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
2620
|
+
# caption_highlight: {}
|
|
2621
|
+
# transition_kit: {}
|
|
2622
|
+
# palette: {}
|
|
2623
|
+
# audio_policy: {}
|
|
2624
|
+
# aspect_targets: []
|
|
2625
|
+
`;
|
|
2626
|
+
}
|
|
2627
|
+
function applyRecipeToTrimOptions(recipe, cliOptions) {
|
|
2628
|
+
const policy = recipe?.silence_policy;
|
|
2629
|
+
if (!policy) return cliOptions;
|
|
2630
|
+
return {
|
|
2631
|
+
...cliOptions,
|
|
2632
|
+
noise: cliOptions.noise ?? policy.noise,
|
|
2633
|
+
minSilence: cliOptions.minSilence ?? policy.min_silence,
|
|
2634
|
+
padPre: cliOptions.padPre ?? policy.default_padding_pre,
|
|
2635
|
+
padPost: cliOptions.padPost ?? policy.default_padding_post,
|
|
2636
|
+
// matchTolerance is recipe-only at this layer (no CLI flag yet)
|
|
2637
|
+
matchTolerance: cliOptions.matchTolerance ?? policy.match_tolerance
|
|
2638
|
+
};
|
|
2639
|
+
}
|
|
2640
|
+
function applyRecipeToCaptionOptions(recipe) {
|
|
2641
|
+
if (!recipe) return {};
|
|
2642
|
+
const style = recipe.caption_style;
|
|
2643
|
+
const grouping = recipe.caption_grouping;
|
|
2644
|
+
const runtimeStyle = {};
|
|
2645
|
+
if (style) {
|
|
2646
|
+
if (style.font_family !== void 0) runtimeStyle.fontFamily = style.font_family;
|
|
2647
|
+
if (style.font_size !== void 0) runtimeStyle.fontSize = style.font_size;
|
|
2648
|
+
if (style.font_weight !== void 0) runtimeStyle.fontWeight = style.font_weight;
|
|
2649
|
+
if (style.text_align !== void 0) runtimeStyle.textAlign = style.text_align;
|
|
2650
|
+
if (style.color !== void 0) runtimeStyle.color = style.color;
|
|
2651
|
+
if (style.y_ratio !== void 0) runtimeStyle.yRatio = style.y_ratio;
|
|
2652
|
+
if (style.width_ratio !== void 0) runtimeStyle.widthRatio = style.width_ratio;
|
|
2653
|
+
if (style.fade_seconds !== void 0) runtimeStyle.fadeSeconds = style.fade_seconds;
|
|
2654
|
+
}
|
|
2655
|
+
return {
|
|
2656
|
+
...Object.keys(runtimeStyle).length > 0 && { style: runtimeStyle },
|
|
2657
|
+
...grouping?.max_words !== void 0 && { maxWords: grouping.max_words },
|
|
2658
|
+
...grouping?.pause_gap !== void 0 && { pauseGap: grouping.pause_gap }
|
|
2659
|
+
};
|
|
2660
|
+
}
|
|
2661
|
+
function applyRecipeToTranscribeOptions(recipe, cliOptions) {
|
|
2662
|
+
if (!recipe) return cliOptions;
|
|
2663
|
+
const captionOptions = applyRecipeToCaptionOptions(recipe);
|
|
2664
|
+
return {
|
|
2665
|
+
...cliOptions,
|
|
2666
|
+
captionOptions
|
|
2667
|
+
};
|
|
2668
|
+
}
|
|
2669
|
+
function renderRecipeWithDefaults(recipe) {
|
|
2670
|
+
const defaults = {
|
|
2671
|
+
silence_policy: {
|
|
2672
|
+
noise: "-30dB",
|
|
2673
|
+
min_silence: 0.35,
|
|
2674
|
+
default_padding_pre: 0.08,
|
|
2675
|
+
default_padding_post: 0.12,
|
|
2676
|
+
match_tolerance: 0.5
|
|
2677
|
+
},
|
|
2678
|
+
caption_style: {
|
|
2679
|
+
font_family: "Inter",
|
|
2680
|
+
font_size: 84,
|
|
2681
|
+
font_weight: "bold",
|
|
2682
|
+
text_align: "center",
|
|
2683
|
+
color: "#FFFFFF",
|
|
2684
|
+
y_ratio: 0.85,
|
|
2685
|
+
width_ratio: 0.9,
|
|
2686
|
+
fade_seconds: 0.05
|
|
2687
|
+
},
|
|
2688
|
+
caption_grouping: {
|
|
2689
|
+
max_words: 5,
|
|
2690
|
+
pause_gap: 0.4
|
|
2691
|
+
}
|
|
2692
|
+
};
|
|
2693
|
+
return {
|
|
2694
|
+
...recipe,
|
|
2695
|
+
silence_policy: { ...defaults.silence_policy, ...recipe.silence_policy },
|
|
2696
|
+
caption_style: { ...defaults.caption_style, ...recipe.caption_style },
|
|
2697
|
+
caption_grouping: { ...defaults.caption_grouping, ...recipe.caption_grouping }
|
|
2698
|
+
};
|
|
2699
|
+
}
|
|
2700
|
+
function recipeToYaml(recipe) {
|
|
2701
|
+
return (0, import_yaml2.stringify)(recipe);
|
|
2702
|
+
}
|
|
2703
|
+
var DEFAULT_OVERLAY_MARGIN = 24;
|
|
2704
|
+
var DEFAULT_OVERLAY_TEXT_STYLE = {
|
|
2705
|
+
fontFamily: "Inter",
|
|
2706
|
+
fontSize: 24,
|
|
2707
|
+
fontWeight: 600,
|
|
2708
|
+
color: "#F5F5F7"
|
|
2709
|
+
};
|
|
2710
|
+
var HANDLE_LAYER_ID = "overlay-handle";
|
|
2711
|
+
var PAGE_NUMBER_LAYER_ID = "overlay-page-number";
|
|
2712
|
+
function applyRecipeToOverlay(doc, recipe, ctx) {
|
|
2713
|
+
const preserved = doc.layers.filter((l) => !(l.tags ?? []).includes("overlay"));
|
|
2714
|
+
const overlayLayers = [];
|
|
2715
|
+
const rules = recipe.overlay_rules;
|
|
2716
|
+
if (!rules) {
|
|
2717
|
+
return { ...doc, layers: preserved };
|
|
2718
|
+
}
|
|
2719
|
+
if (rules.handle) {
|
|
2720
|
+
overlayLayers.push(buildHandleLayer(rules.handle, doc.canvas));
|
|
2721
|
+
}
|
|
2722
|
+
if (rules.page_number) {
|
|
2723
|
+
if (ctx?.currentIndex != null && ctx?.totalCount != null) {
|
|
2724
|
+
overlayLayers.push(
|
|
2725
|
+
buildPageNumberLayer(rules.page_number, doc.canvas, ctx.currentIndex, ctx.totalCount)
|
|
2726
|
+
);
|
|
2727
|
+
} else {
|
|
2728
|
+
console.warn(
|
|
2729
|
+
`applyRecipeToOverlay: recipe.overlay_rules.page_number present but currentIndex/totalCount not provided \u2014 skipping page_number layer.`
|
|
2730
|
+
);
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
return {
|
|
2734
|
+
...doc,
|
|
2735
|
+
layers: [...preserved, ...overlayLayers]
|
|
2736
|
+
};
|
|
2737
|
+
}
|
|
2738
|
+
function buildHandleLayer(rule, canvas) {
|
|
2739
|
+
const margin = rule.margin ?? DEFAULT_OVERLAY_MARGIN;
|
|
2740
|
+
const { frame, anchorPoint } = anchorToFrame(rule.anchor, canvas, margin);
|
|
2741
|
+
return {
|
|
2742
|
+
id: HANDLE_LAYER_ID,
|
|
2743
|
+
tags: ["overlay"],
|
|
2744
|
+
visual: {
|
|
2745
|
+
type: "text",
|
|
2746
|
+
content: rule.text,
|
|
2747
|
+
style: mergeOverlayStyle(rule.style)
|
|
2748
|
+
},
|
|
2749
|
+
frame,
|
|
2750
|
+
bounds: { width: 600, height: 80 },
|
|
2751
|
+
anchorPoint
|
|
2752
|
+
};
|
|
2753
|
+
}
|
|
2754
|
+
function buildPageNumberLayer(rule, canvas, currentIndex, totalCount) {
|
|
2755
|
+
const margin = rule.margin ?? DEFAULT_OVERLAY_MARGIN;
|
|
2756
|
+
const { frame, anchorPoint } = anchorToFrame(rule.anchor, canvas, margin);
|
|
2757
|
+
return {
|
|
2758
|
+
id: PAGE_NUMBER_LAYER_ID,
|
|
2759
|
+
tags: ["overlay"],
|
|
2760
|
+
visual: {
|
|
2761
|
+
type: "text",
|
|
2762
|
+
content: renderPageNumberFormat(rule.format, currentIndex, totalCount),
|
|
2763
|
+
style: mergeOverlayStyle(rule.style)
|
|
2764
|
+
},
|
|
2765
|
+
frame,
|
|
2766
|
+
bounds: { width: 200, height: 80 },
|
|
2767
|
+
anchorPoint
|
|
2768
|
+
};
|
|
2769
|
+
}
|
|
2770
|
+
function anchorToFrame(anchor, canvas, margin) {
|
|
2771
|
+
switch (anchor) {
|
|
2772
|
+
case "top-left":
|
|
2773
|
+
return { frame: { x: margin, y: margin }, anchorPoint: { x: 0, y: 0 } };
|
|
2774
|
+
case "top-right":
|
|
2775
|
+
return { frame: { x: canvas.width - margin, y: margin }, anchorPoint: { x: 1, y: 0 } };
|
|
2776
|
+
case "bottom-left":
|
|
2777
|
+
return { frame: { x: margin, y: canvas.height - margin }, anchorPoint: { x: 0, y: 1 } };
|
|
2778
|
+
case "bottom-right":
|
|
2779
|
+
return {
|
|
2780
|
+
frame: { x: canvas.width - margin, y: canvas.height - margin },
|
|
2781
|
+
anchorPoint: { x: 1, y: 1 }
|
|
2782
|
+
};
|
|
2783
|
+
}
|
|
2784
|
+
}
|
|
2785
|
+
function mergeOverlayStyle(style) {
|
|
2786
|
+
return {
|
|
2787
|
+
fontFamily: style?.font_family ?? DEFAULT_OVERLAY_TEXT_STYLE.fontFamily,
|
|
2788
|
+
fontSize: style?.font_size ?? DEFAULT_OVERLAY_TEXT_STYLE.fontSize,
|
|
2789
|
+
fontWeight: style?.font_weight ?? DEFAULT_OVERLAY_TEXT_STYLE.fontWeight,
|
|
2790
|
+
color: style?.color ?? DEFAULT_OVERLAY_TEXT_STYLE.color
|
|
2791
|
+
};
|
|
2792
|
+
}
|
|
2793
|
+
function renderPageNumberFormat(format, currentIndex, totalCount) {
|
|
2794
|
+
return format.replace(
|
|
2795
|
+
/\{(current|total)(?::0(\d+)d)?\}/g,
|
|
2796
|
+
(_, name, padWidth) => {
|
|
2797
|
+
const value = name === "current" ? currentIndex : totalCount;
|
|
2798
|
+
const str = String(value);
|
|
2799
|
+
if (padWidth) {
|
|
2800
|
+
const width = parseInt(padWidth, 10);
|
|
2801
|
+
return str.padStart(width, "0");
|
|
2802
|
+
}
|
|
2803
|
+
return str;
|
|
2804
|
+
}
|
|
2805
|
+
);
|
|
2806
|
+
}
|
|
2807
|
+
|
|
2808
|
+
// src/commands/trim.ts
|
|
2809
|
+
async function trimProject(projectDir, options = {}) {
|
|
2810
|
+
const project = loadVideoProject(projectDir);
|
|
2811
|
+
await probeSilencedetect();
|
|
2812
|
+
const duration = await probeDuration(project.sourcePath);
|
|
2813
|
+
const silences = await runSilenceDetect(project.sourcePath, {
|
|
2814
|
+
noise: options.noise,
|
|
2815
|
+
minSilence: options.minSilence
|
|
2816
|
+
});
|
|
2817
|
+
const speech = invertToSpeechIntervals(silences, duration);
|
|
2818
|
+
const padPre = options.padPre ?? DEFAULT_PADDING_PRE;
|
|
2819
|
+
const padPost = options.padPost ?? DEFAULT_PADDING_POST;
|
|
2820
|
+
let cuts = buildInitialCuts(speech, padPre, padPost);
|
|
2821
|
+
if (!options.reset) {
|
|
2822
|
+
const existing = readCutList(project);
|
|
2823
|
+
cuts = mergeWithExisting(cuts, existing.cuts, options.matchTolerance);
|
|
2824
|
+
}
|
|
2825
|
+
if (typeof options.tightenMs === "number") {
|
|
2826
|
+
applyGlobalPadding(cuts, -options.tightenMs / 1e3);
|
|
2827
|
+
}
|
|
2828
|
+
if (typeof options.loosenMs === "number") {
|
|
2829
|
+
applyGlobalPadding(cuts, options.loosenMs / 1e3);
|
|
2830
|
+
}
|
|
2831
|
+
if (typeof options.cutIndex === "number") {
|
|
2832
|
+
if (options.cutIndex < 0 || options.cutIndex >= cuts.length) {
|
|
2833
|
+
throw new Error(
|
|
2834
|
+
`--cut ${options.cutIndex} out of range (have ${cuts.length} cuts)`
|
|
2835
|
+
);
|
|
2836
|
+
}
|
|
2837
|
+
if (options.padPre !== void 0) cuts[options.cutIndex].paddingPre = options.padPre;
|
|
2838
|
+
if (options.padPost !== void 0) cuts[options.cutIndex].paddingPost = options.padPost;
|
|
2839
|
+
}
|
|
2840
|
+
resolveOverlaps(cuts);
|
|
2841
|
+
clampBoundaries(cuts, duration);
|
|
2842
|
+
const result = {
|
|
2843
|
+
projectDir: project.dir,
|
|
2844
|
+
duration,
|
|
2845
|
+
cuts,
|
|
2846
|
+
layerCount: cuts.length
|
|
2847
|
+
};
|
|
2848
|
+
if (options.dryRun) return result;
|
|
2849
|
+
writeCutList(project, {
|
|
2850
|
+
version: "1.1",
|
|
2851
|
+
source: project.manifest.source,
|
|
2852
|
+
cuts
|
|
2853
|
+
});
|
|
2854
|
+
const doc = readComposition(project);
|
|
2855
|
+
const updated = rewriteCutLayers(doc, cuts, project.manifest.source, duration);
|
|
2856
|
+
writeComposition(project, updated);
|
|
2857
|
+
return result;
|
|
2858
|
+
}
|
|
2859
|
+
function formatResult2(result) {
|
|
2860
|
+
const lines = [];
|
|
2861
|
+
lines.push(`Trimmed ${result.projectDir}`);
|
|
2862
|
+
lines.push(` source duration: ${result.duration.toFixed(2)}s`);
|
|
2863
|
+
lines.push(` cuts: ${result.cuts.length}`);
|
|
2864
|
+
for (let i = 0; i < result.cuts.length; i++) {
|
|
2865
|
+
const c = result.cuts[i];
|
|
2866
|
+
const effStart = Math.max(0, c.rawStart - c.paddingPre);
|
|
2867
|
+
const effEnd = Math.min(result.duration, c.rawEnd + c.paddingPost);
|
|
2868
|
+
const dur = effEnd - effStart;
|
|
2869
|
+
lines.push(
|
|
2870
|
+
` [${i}] ${effStart.toFixed(2)}s \u2192 ${effEnd.toFixed(2)}s (${dur.toFixed(2)}s, pad ${c.paddingPre.toFixed(2)}/${c.paddingPost.toFixed(2)})` + (c.label ? ` "${c.label}"` : "")
|
|
2871
|
+
);
|
|
2872
|
+
}
|
|
2873
|
+
return lines.join("\n");
|
|
2874
|
+
}
|
|
2875
|
+
function trimCommand(program2) {
|
|
2876
|
+
program2.command("trim <project>").description(
|
|
2877
|
+
"Detect silence and rewrite project.atelier with parametric cuts (silence-trim tagged layers). Preserves user padding overrides on re-run."
|
|
2878
|
+
).option("--noise <dB>", "Silence threshold (default: -30dB)", "-30dB").option(
|
|
2879
|
+
"--min-silence <seconds>",
|
|
2880
|
+
"Minimum silence duration to register (default: 0.35)",
|
|
2881
|
+
(v) => parseFloat(v),
|
|
2882
|
+
0.35
|
|
2883
|
+
).option(
|
|
2884
|
+
"--pad-pre <seconds>",
|
|
2885
|
+
`Default leading padding (default: ${DEFAULT_PADDING_PRE})`,
|
|
2886
|
+
(v) => parseFloat(v)
|
|
2887
|
+
).option(
|
|
2888
|
+
"--pad-post <seconds>",
|
|
2889
|
+
`Default trailing padding (default: ${DEFAULT_PADDING_POST})`,
|
|
2890
|
+
(v) => parseFloat(v)
|
|
2891
|
+
).option("--tighten <ms>", "Reduce all current padding by N ms", (v) => parseInt(v, 10)).option("--loosen <ms>", "Increase all current padding by N ms", (v) => parseInt(v, 10)).option(
|
|
2892
|
+
"--cut <index>",
|
|
2893
|
+
"Apply --pad-pre / --pad-post to one specific cut only",
|
|
2894
|
+
(v) => parseInt(v, 10)
|
|
2895
|
+
).option("--reset", "Discard existing padding; re-detect from scratch").option("--recipe <name>", "Apply a Studio Recipe's silence_policy as the baseline").option("--dry-run", "Print computed cuts; don't write files").option("--json", "Output result as JSON for piping").action(async (project, opts) => {
|
|
2896
|
+
try {
|
|
2897
|
+
let trimOpts = {
|
|
2898
|
+
noise: opts.noise,
|
|
2899
|
+
minSilence: opts.minSilence,
|
|
2900
|
+
padPre: opts.padPre,
|
|
2901
|
+
padPost: opts.padPost,
|
|
2902
|
+
tightenMs: opts.tighten,
|
|
2903
|
+
loosenMs: opts.loosen,
|
|
2904
|
+
cutIndex: opts.cut,
|
|
2905
|
+
reset: opts.reset,
|
|
2906
|
+
dryRun: opts.dryRun
|
|
2907
|
+
};
|
|
2908
|
+
if (opts.recipe) {
|
|
2909
|
+
const { recipe } = loadRecipe(opts.recipe, project);
|
|
2910
|
+
trimOpts = applyRecipeToTrimOptions(recipe, trimOpts);
|
|
2911
|
+
}
|
|
2912
|
+
const result = await trimProject(project, trimOpts);
|
|
2913
|
+
if (opts.json) {
|
|
2914
|
+
console.log(JSON.stringify(result, null, 2));
|
|
2915
|
+
} else {
|
|
2916
|
+
console.log(formatResult2(result));
|
|
2917
|
+
if (opts.dryRun) console.log("(dry-run \u2014 no files written)");
|
|
2918
|
+
}
|
|
2919
|
+
} catch (err) {
|
|
2920
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2921
|
+
console.error(`atelier trim: ${msg}`);
|
|
2922
|
+
process.exit(1);
|
|
2923
|
+
}
|
|
2924
|
+
});
|
|
2925
|
+
}
|
|
2926
|
+
|
|
2927
|
+
// src/lib/whisper.ts
|
|
2928
|
+
var import_node_child_process2 = require("child_process");
|
|
2929
|
+
var import_node_fs5 = require("fs");
|
|
2930
|
+
async function probeWhisper() {
|
|
2931
|
+
if (await commandExists("whisper-cli")) return "whisper-cpp";
|
|
2932
|
+
if (process.env.OPENAI_API_KEY) return "openai-api";
|
|
2933
|
+
return "none";
|
|
2934
|
+
}
|
|
2935
|
+
async function runWhisperCpp(sourcePath, options = {}) {
|
|
2936
|
+
const model = options.modelPath ?? options.model ?? "base.en";
|
|
2937
|
+
const args = [
|
|
2938
|
+
sourcePath,
|
|
2939
|
+
"--model",
|
|
2940
|
+
model,
|
|
2941
|
+
"--output-json",
|
|
2942
|
+
"--word-thold",
|
|
2943
|
+
"0.01",
|
|
2944
|
+
// emit word-level timestamps
|
|
2945
|
+
"--print-progress",
|
|
2946
|
+
"false"
|
|
2947
|
+
];
|
|
2948
|
+
if (options.language) {
|
|
2949
|
+
args.push("--language", options.language);
|
|
2950
|
+
}
|
|
2951
|
+
return runCaptureStdout("whisper-cli", args);
|
|
2952
|
+
}
|
|
2953
|
+
function parseWhisperCppJson(jsonStr) {
|
|
2954
|
+
const raw = JSON.parse(jsonStr);
|
|
2955
|
+
const segments = (raw.transcription ?? []).map((seg) => {
|
|
2956
|
+
const segStart = (seg.offsets?.from ?? 0) / 1e3;
|
|
2957
|
+
const segEnd = (seg.offsets?.to ?? 0) / 1e3;
|
|
2958
|
+
const segText = seg.text.trim();
|
|
2959
|
+
let words;
|
|
2960
|
+
if (seg.tokens && seg.tokens.length > 0) {
|
|
2961
|
+
words = seg.tokens.filter((t) => t.text.trim().length > 0 && !t.text.startsWith("[_")).map((t) => ({
|
|
2962
|
+
detected: t.text.trim(),
|
|
2963
|
+
text: t.text.trim(),
|
|
2964
|
+
start: (t.offsets?.from ?? segStart * 1e3) / 1e3,
|
|
2965
|
+
end: (t.offsets?.to ?? segEnd * 1e3) / 1e3,
|
|
2966
|
+
...t.p !== void 0 && { confidence: t.p }
|
|
2967
|
+
}));
|
|
2968
|
+
} else {
|
|
2969
|
+
const tokens = segText.split(/\s+/).filter((t) => t.length > 0);
|
|
2970
|
+
const span = segEnd - segStart;
|
|
2971
|
+
const per = tokens.length > 0 ? span / tokens.length : 0;
|
|
2972
|
+
words = tokens.map((tok, i) => ({
|
|
2973
|
+
detected: tok,
|
|
2974
|
+
text: tok,
|
|
2975
|
+
start: segStart + i * per,
|
|
2976
|
+
end: segStart + (i + 1) * per
|
|
2977
|
+
}));
|
|
2978
|
+
}
|
|
2979
|
+
return {
|
|
2980
|
+
text: segText,
|
|
2981
|
+
start: segStart,
|
|
2982
|
+
end: segEnd,
|
|
2983
|
+
words
|
|
2984
|
+
};
|
|
2985
|
+
});
|
|
2986
|
+
return {
|
|
2987
|
+
version: "1.1",
|
|
2988
|
+
...raw.result?.language !== void 0 && { language: raw.result.language },
|
|
2989
|
+
segments
|
|
2990
|
+
};
|
|
2991
|
+
}
|
|
2992
|
+
async function commandExists(name) {
|
|
2993
|
+
if (name.startsWith("/") || name.match(/^[A-Z]:\\/)) {
|
|
2994
|
+
return (0, import_node_fs5.existsSync)(name);
|
|
2995
|
+
}
|
|
2996
|
+
return new Promise((resolve16) => {
|
|
2997
|
+
const probe = (0, import_node_child_process2.spawn)(process.platform === "win32" ? "where" : "which", [name], {
|
|
2998
|
+
stdio: ["ignore", "ignore", "ignore"]
|
|
2999
|
+
});
|
|
3000
|
+
probe.on("error", () => resolve16(false));
|
|
3001
|
+
probe.on("close", (code) => resolve16(code === 0));
|
|
3002
|
+
});
|
|
3003
|
+
}
|
|
3004
|
+
function runCaptureStdout(cmd, args) {
|
|
3005
|
+
return new Promise((resolve16, reject) => {
|
|
3006
|
+
const proc = (0, import_node_child_process2.spawn)(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
3007
|
+
let stdout = "";
|
|
3008
|
+
let stderr = "";
|
|
3009
|
+
proc.stdout.on("data", (b) => stdout += b.toString());
|
|
3010
|
+
proc.stderr.on("data", (b) => stderr += b.toString());
|
|
3011
|
+
proc.on("error", (err) => {
|
|
3012
|
+
const e = err;
|
|
3013
|
+
if (e.code === "ENOENT") {
|
|
3014
|
+
reject(new Error(
|
|
3015
|
+
`${cmd} not found on PATH. Install whisper.cpp (brew install whisper-cpp) or run \`pnpm add @xenova/transformers\` for the ONNX fallback.`
|
|
3016
|
+
));
|
|
3017
|
+
} else {
|
|
3018
|
+
reject(err);
|
|
3019
|
+
}
|
|
3020
|
+
});
|
|
3021
|
+
proc.on("close", (code) => {
|
|
3022
|
+
if (code !== 0) {
|
|
3023
|
+
reject(new Error(`${cmd} exited ${code}
|
|
3024
|
+
${stderr}`));
|
|
3025
|
+
} else {
|
|
3026
|
+
resolve16(stdout);
|
|
3027
|
+
}
|
|
3028
|
+
});
|
|
3029
|
+
});
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
// src/lib/transcript-model.ts
|
|
3033
|
+
var DEFAULT_TRANSCRIPT_MATCH_TOLERANCE = 0.3;
|
|
3034
|
+
var DEFAULT_PHRASE_MAX_WORDS = 5;
|
|
3035
|
+
var DEFAULT_PHRASE_PAUSE_GAP_SECONDS = 0.4;
|
|
3036
|
+
var END_PUNCT = /[.!?,;:]\s*$/;
|
|
3037
|
+
function flattenWords(transcript) {
|
|
3038
|
+
return transcript.segments.flatMap((s) => s.words);
|
|
3039
|
+
}
|
|
3040
|
+
function groupIntoPhrases(transcript, options = {}) {
|
|
3041
|
+
const maxWords = options.maxWords ?? DEFAULT_PHRASE_MAX_WORDS;
|
|
3042
|
+
const pauseGap = options.pauseGap ?? DEFAULT_PHRASE_PAUSE_GAP_SECONDS;
|
|
3043
|
+
const phrases = [];
|
|
3044
|
+
const visibleWords = flattenWords(transcript).filter((w) => !w.hidden);
|
|
3045
|
+
let current = [];
|
|
3046
|
+
for (let i = 0; i < visibleWords.length; i++) {
|
|
3047
|
+
const w = visibleWords[i];
|
|
3048
|
+
current.push(w);
|
|
3049
|
+
const next = visibleWords[i + 1];
|
|
3050
|
+
const gap = next ? next.start - w.end : 0;
|
|
3051
|
+
const endsOnPunct = END_PUNCT.test(w.text);
|
|
3052
|
+
const atMax = current.length >= maxWords;
|
|
3053
|
+
if (!next || atMax || endsOnPunct || gap > pauseGap) {
|
|
3054
|
+
phrases.push({
|
|
3055
|
+
start: current[0].start,
|
|
3056
|
+
end: current[current.length - 1].end,
|
|
3057
|
+
text: current.map((c) => c.text).join(" "),
|
|
3058
|
+
words: current
|
|
3059
|
+
});
|
|
3060
|
+
current = [];
|
|
3061
|
+
}
|
|
3062
|
+
}
|
|
3063
|
+
return phrases;
|
|
3064
|
+
}
|
|
3065
|
+
function mergeTranscriptWithExisting(fresh, existing, tolerance = DEFAULT_TRANSCRIPT_MATCH_TOLERANCE) {
|
|
3066
|
+
const existingWords = flattenWords(existing);
|
|
3067
|
+
const merged = {
|
|
3068
|
+
version: "1.1",
|
|
3069
|
+
...fresh.language !== void 0 && { language: fresh.language },
|
|
3070
|
+
segments: fresh.segments.map((seg) => ({
|
|
3071
|
+
text: seg.text,
|
|
3072
|
+
start: seg.start,
|
|
3073
|
+
end: seg.end,
|
|
3074
|
+
words: seg.words.map((freshWord) => {
|
|
3075
|
+
const match = existingWords.find(
|
|
3076
|
+
(e) => !e.userAdded && Math.abs(e.start - freshWord.start) < tolerance && e.detected === freshWord.detected
|
|
3077
|
+
);
|
|
3078
|
+
if (!match) return freshWord;
|
|
3079
|
+
return {
|
|
3080
|
+
detected: freshWord.detected,
|
|
3081
|
+
text: match.text,
|
|
3082
|
+
start: freshWord.start,
|
|
3083
|
+
end: freshWord.end,
|
|
3084
|
+
...freshWord.confidence !== void 0 && { confidence: freshWord.confidence },
|
|
3085
|
+
...match.userEdited && { userEdited: true },
|
|
3086
|
+
...match.hidden && { hidden: true }
|
|
3087
|
+
};
|
|
3088
|
+
})
|
|
3089
|
+
}))
|
|
3090
|
+
};
|
|
3091
|
+
const orphans = existingWords.filter((w) => w.userAdded);
|
|
3092
|
+
for (const orphan of orphans) {
|
|
3093
|
+
const segIdx = merged.segments.findIndex(
|
|
3094
|
+
(s) => orphan.start >= s.start && orphan.start <= s.end
|
|
3095
|
+
);
|
|
3096
|
+
const targetSeg = segIdx >= 0 ? merged.segments[segIdx] : merged.segments[merged.segments.length - 1];
|
|
3097
|
+
if (!targetSeg) continue;
|
|
3098
|
+
const insertIdx = targetSeg.words.findIndex((w) => w.start > orphan.start);
|
|
3099
|
+
if (insertIdx === -1) targetSeg.words.push(orphan);
|
|
3100
|
+
else targetSeg.words.splice(insertIdx, 0, orphan);
|
|
3101
|
+
}
|
|
3102
|
+
return merged;
|
|
3103
|
+
}
|
|
3104
|
+
function applyTextEdit(transcript, wordIndex, newText) {
|
|
3105
|
+
return mapWord(transcript, wordIndex, (w) => ({
|
|
3106
|
+
...w,
|
|
3107
|
+
text: newText,
|
|
3108
|
+
userEdited: true
|
|
3109
|
+
}));
|
|
3110
|
+
}
|
|
3111
|
+
function applyBatchReplace(transcript, find, replace) {
|
|
3112
|
+
return {
|
|
3113
|
+
...transcript,
|
|
3114
|
+
segments: transcript.segments.map((seg) => ({
|
|
3115
|
+
...seg,
|
|
3116
|
+
words: seg.words.map(
|
|
3117
|
+
(w) => w.detected === find ? { ...w, text: replace, userEdited: true } : w
|
|
3118
|
+
)
|
|
3119
|
+
}))
|
|
3120
|
+
};
|
|
3121
|
+
}
|
|
3122
|
+
function applyHide(transcript, wordIndex) {
|
|
3123
|
+
return mapWord(transcript, wordIndex, (w) => ({ ...w, hidden: true }));
|
|
3124
|
+
}
|
|
3125
|
+
function applyAdd(transcript, afterIndex, text, duration = 0.15) {
|
|
3126
|
+
const flat = flattenWords(transcript);
|
|
3127
|
+
if (afterIndex < 0 || afterIndex >= flat.length) {
|
|
3128
|
+
throw new Error(`afterIndex ${afterIndex} out of range (have ${flat.length} words)`);
|
|
3129
|
+
}
|
|
3130
|
+
const anchor = flat[afterIndex];
|
|
3131
|
+
const newWord = {
|
|
3132
|
+
detected: text,
|
|
3133
|
+
text,
|
|
3134
|
+
start: anchor.end,
|
|
3135
|
+
end: anchor.end + duration,
|
|
3136
|
+
userAdded: true
|
|
3137
|
+
};
|
|
3138
|
+
let cursor = 0;
|
|
3139
|
+
return {
|
|
3140
|
+
...transcript,
|
|
3141
|
+
segments: transcript.segments.map((seg) => {
|
|
3142
|
+
const segStart = cursor;
|
|
3143
|
+
cursor += seg.words.length;
|
|
3144
|
+
if (afterIndex < segStart || afterIndex >= cursor) return seg;
|
|
3145
|
+
const localIdx = afterIndex - segStart;
|
|
3146
|
+
return {
|
|
3147
|
+
...seg,
|
|
3148
|
+
words: [...seg.words.slice(0, localIdx + 1), newWord, ...seg.words.slice(localIdx + 1)]
|
|
3149
|
+
};
|
|
3150
|
+
})
|
|
3151
|
+
};
|
|
3152
|
+
}
|
|
3153
|
+
function applyMerge(transcript, firstIndex) {
|
|
3154
|
+
const flat = flattenWords(transcript);
|
|
3155
|
+
if (firstIndex < 0 || firstIndex >= flat.length - 1) {
|
|
3156
|
+
throw new Error(`firstIndex ${firstIndex} out of range for merge`);
|
|
3157
|
+
}
|
|
3158
|
+
const a = flat[firstIndex];
|
|
3159
|
+
const b = flat[firstIndex + 1];
|
|
3160
|
+
const mergedWord = {
|
|
3161
|
+
detected: a.detected + b.detected,
|
|
3162
|
+
text: a.text + b.text,
|
|
3163
|
+
start: a.start,
|
|
3164
|
+
end: b.end,
|
|
3165
|
+
userEdited: true
|
|
3166
|
+
};
|
|
3167
|
+
let cursor = 0;
|
|
3168
|
+
return {
|
|
3169
|
+
...transcript,
|
|
3170
|
+
segments: transcript.segments.map((seg) => {
|
|
3171
|
+
const segStart = cursor;
|
|
3172
|
+
cursor += seg.words.length;
|
|
3173
|
+
if (firstIndex < segStart || firstIndex >= cursor - 1) return seg;
|
|
3174
|
+
const localIdx = firstIndex - segStart;
|
|
3175
|
+
return {
|
|
3176
|
+
...seg,
|
|
3177
|
+
words: [...seg.words.slice(0, localIdx), mergedWord, ...seg.words.slice(localIdx + 2)]
|
|
3178
|
+
};
|
|
3179
|
+
})
|
|
3180
|
+
};
|
|
3181
|
+
}
|
|
3182
|
+
function applySplit(transcript, wordIndex, fraction, firstText, secondText) {
|
|
3183
|
+
if (fraction <= 0 || fraction >= 1) {
|
|
3184
|
+
throw new Error(`split fraction must be in (0, 1), got ${fraction}`);
|
|
3185
|
+
}
|
|
3186
|
+
const flat = flattenWords(transcript);
|
|
3187
|
+
if (wordIndex < 0 || wordIndex >= flat.length) {
|
|
3188
|
+
throw new Error(`wordIndex ${wordIndex} out of range`);
|
|
3189
|
+
}
|
|
3190
|
+
const w = flat[wordIndex];
|
|
3191
|
+
const splitTime = w.start + (w.end - w.start) * fraction;
|
|
3192
|
+
const cutChar = Math.max(1, Math.floor(w.text.length * fraction));
|
|
3193
|
+
const first = {
|
|
3194
|
+
detected: w.detected,
|
|
3195
|
+
text: firstText ?? w.text.slice(0, cutChar),
|
|
3196
|
+
start: w.start,
|
|
3197
|
+
end: splitTime,
|
|
3198
|
+
userEdited: true
|
|
3199
|
+
};
|
|
3200
|
+
const second = {
|
|
3201
|
+
detected: w.detected,
|
|
3202
|
+
text: secondText ?? w.text.slice(cutChar),
|
|
3203
|
+
start: splitTime,
|
|
3204
|
+
end: w.end,
|
|
3205
|
+
userEdited: true
|
|
3206
|
+
};
|
|
3207
|
+
let cursor = 0;
|
|
3208
|
+
return {
|
|
3209
|
+
...transcript,
|
|
3210
|
+
segments: transcript.segments.map((seg) => {
|
|
3211
|
+
const segStart = cursor;
|
|
3212
|
+
cursor += seg.words.length;
|
|
3213
|
+
if (wordIndex < segStart || wordIndex >= cursor) return seg;
|
|
3214
|
+
const localIdx = wordIndex - segStart;
|
|
3215
|
+
return {
|
|
3216
|
+
...seg,
|
|
3217
|
+
words: [...seg.words.slice(0, localIdx), first, second, ...seg.words.slice(localIdx + 1)]
|
|
3218
|
+
};
|
|
3219
|
+
})
|
|
3220
|
+
};
|
|
3221
|
+
}
|
|
3222
|
+
function mapWord(transcript, wordIndex, fn) {
|
|
3223
|
+
const flat = flattenWords(transcript);
|
|
3224
|
+
if (wordIndex < 0 || wordIndex >= flat.length) {
|
|
3225
|
+
throw new Error(`wordIndex ${wordIndex} out of range (have ${flat.length} words)`);
|
|
3226
|
+
}
|
|
3227
|
+
let cursor = 0;
|
|
3228
|
+
return {
|
|
3229
|
+
...transcript,
|
|
3230
|
+
segments: transcript.segments.map((seg) => {
|
|
3231
|
+
const segStart = cursor;
|
|
3232
|
+
cursor += seg.words.length;
|
|
3233
|
+
if (wordIndex < segStart || wordIndex >= cursor) return seg;
|
|
3234
|
+
const localIdx = wordIndex - segStart;
|
|
3235
|
+
return {
|
|
3236
|
+
...seg,
|
|
3237
|
+
words: seg.words.map((w, i) => i === localIdx ? fn(w) : w)
|
|
3238
|
+
};
|
|
3239
|
+
})
|
|
3240
|
+
};
|
|
3241
|
+
}
|
|
3242
|
+
|
|
3243
|
+
// src/lib/caption-builder.ts
|
|
3244
|
+
var DEFAULT_STYLE = {
|
|
3245
|
+
fontFamily: "Inter",
|
|
3246
|
+
fontSize: 84,
|
|
3247
|
+
fontWeight: "bold",
|
|
3248
|
+
textAlign: "center",
|
|
3249
|
+
color: "#FFFFFF",
|
|
3250
|
+
yRatio: 0.85,
|
|
3251
|
+
widthRatio: 0.9,
|
|
3252
|
+
fadeSeconds: 0.05
|
|
3253
|
+
};
|
|
3254
|
+
function buildCaptionLayers(transcript, canvas, options = {}) {
|
|
3255
|
+
const style = { ...DEFAULT_STYLE, ...options.style };
|
|
3256
|
+
const phrases = groupIntoPhrases(transcript, {
|
|
3257
|
+
maxWords: options.maxWords,
|
|
3258
|
+
pauseGap: options.pauseGap
|
|
3259
|
+
});
|
|
3260
|
+
const layers = [];
|
|
3261
|
+
const deltas = [];
|
|
3262
|
+
const fps = canvas.fps;
|
|
3263
|
+
phrases.forEach((phrase, idx) => {
|
|
3264
|
+
const layerId = `caption-${idx}`;
|
|
3265
|
+
layers.push(buildPhraseLayer(layerId, phrase, canvas, style));
|
|
3266
|
+
deltas.push(...buildPhraseDeltas(layerId, phrase, style.fadeSeconds, fps));
|
|
3267
|
+
});
|
|
3268
|
+
return { layers, deltas };
|
|
3269
|
+
}
|
|
3270
|
+
function rewriteCaptionLayers(doc, transcript, options = {}) {
|
|
3271
|
+
const preserved = doc.layers.filter((l) => !(l.tags ?? []).includes("caption"));
|
|
3272
|
+
const { layers: captionLayers, deltas: captionDeltas } = buildCaptionLayers(
|
|
3273
|
+
transcript,
|
|
3274
|
+
doc.canvas,
|
|
3275
|
+
options
|
|
3276
|
+
);
|
|
3277
|
+
const captionLayerIds = new Set(captionLayers.map((l) => l.id));
|
|
3278
|
+
const stateNames = Object.keys(doc.states);
|
|
3279
|
+
const targetStateName = stateNames.includes("default") ? "default" : stateNames[0];
|
|
3280
|
+
const states = { ...doc.states };
|
|
3281
|
+
if (targetStateName && states[targetStateName]) {
|
|
3282
|
+
const existing = states[targetStateName];
|
|
3283
|
+
const preservedDeltas = existing.deltas.filter(
|
|
3284
|
+
(d) => !captionLayerIds.has(d.layer)
|
|
3285
|
+
);
|
|
3286
|
+
states[targetStateName] = {
|
|
3287
|
+
...existing,
|
|
3288
|
+
deltas: [...preservedDeltas, ...captionDeltas]
|
|
3289
|
+
};
|
|
3290
|
+
}
|
|
3291
|
+
return {
|
|
3292
|
+
...doc,
|
|
3293
|
+
layers: [...preserved, ...captionLayers],
|
|
3294
|
+
states
|
|
3295
|
+
};
|
|
3296
|
+
}
|
|
3297
|
+
function buildPhraseLayer(id, phrase, canvas, style) {
|
|
3298
|
+
return {
|
|
3299
|
+
id,
|
|
3300
|
+
tags: ["caption"],
|
|
3301
|
+
visual: {
|
|
3302
|
+
type: "text",
|
|
3303
|
+
content: phrase.text,
|
|
3304
|
+
style: {
|
|
3305
|
+
fontFamily: style.fontFamily,
|
|
3306
|
+
fontSize: style.fontSize,
|
|
3307
|
+
fontWeight: style.fontWeight,
|
|
3308
|
+
textAlign: style.textAlign,
|
|
3309
|
+
color: style.color
|
|
3310
|
+
}
|
|
3311
|
+
},
|
|
3312
|
+
frame: {
|
|
3313
|
+
x: canvas.width / 2,
|
|
3314
|
+
y: canvas.height * style.yRatio
|
|
3315
|
+
},
|
|
3316
|
+
bounds: {
|
|
3317
|
+
width: canvas.width * style.widthRatio,
|
|
3318
|
+
height: Math.max(120, style.fontSize * 1.6)
|
|
3319
|
+
},
|
|
3320
|
+
anchorPoint: { x: 0.5, y: 0.5 },
|
|
3321
|
+
opacity: 0
|
|
3322
|
+
};
|
|
3323
|
+
}
|
|
3324
|
+
function buildPhraseDeltas(layerId, phrase, fadeSeconds, fps) {
|
|
3325
|
+
const fadeFrames = Math.max(1, Math.round(fadeSeconds * fps));
|
|
3326
|
+
const startFrame = Math.floor(phrase.start * fps);
|
|
3327
|
+
const endFrame = Math.ceil(phrase.end * fps);
|
|
3328
|
+
const fadeOutStart = Math.max(startFrame + 1, endFrame - fadeFrames);
|
|
3329
|
+
return [
|
|
3330
|
+
// Fade in to visible at phrase start
|
|
3331
|
+
{
|
|
3332
|
+
layer: layerId,
|
|
3333
|
+
property: "opacity",
|
|
3334
|
+
range: [Math.max(0, startFrame - fadeFrames), startFrame],
|
|
3335
|
+
from: 0,
|
|
3336
|
+
to: 1,
|
|
3337
|
+
easing: "ease-out"
|
|
3338
|
+
},
|
|
3339
|
+
// Hold visible through the phrase
|
|
3340
|
+
// (no explicit delta needed — value persists between deltas)
|
|
3341
|
+
// Fade out at phrase end
|
|
3342
|
+
{
|
|
3343
|
+
layer: layerId,
|
|
3344
|
+
property: "opacity",
|
|
3345
|
+
range: [fadeOutStart, endFrame],
|
|
3346
|
+
from: 1,
|
|
3347
|
+
to: 0,
|
|
3348
|
+
easing: "ease-in"
|
|
3349
|
+
}
|
|
3350
|
+
];
|
|
3351
|
+
}
|
|
3352
|
+
|
|
3353
|
+
// src/commands/transcribe.ts
|
|
3354
|
+
async function transcribeProject(projectDir, options = {}) {
|
|
3355
|
+
const project = loadVideoProject(projectDir);
|
|
3356
|
+
const backend = await probeWhisper();
|
|
3357
|
+
if (backend === "none") {
|
|
3358
|
+
throw new Error(
|
|
3359
|
+
"No Whisper backend available. Install whisper.cpp (brew install whisper-cpp) or set OPENAI_API_KEY and pass --use-api."
|
|
3360
|
+
);
|
|
3361
|
+
}
|
|
3362
|
+
if (backend === "openai-api") {
|
|
3363
|
+
throw new Error(
|
|
3364
|
+
"OpenAI API backend is not yet implemented. Install whisper.cpp for local transcription."
|
|
3365
|
+
);
|
|
3366
|
+
}
|
|
3367
|
+
const rawJson = await runWhisperCpp(project.sourcePath, {
|
|
3368
|
+
model: options.model,
|
|
3369
|
+
language: options.language
|
|
3370
|
+
});
|
|
3371
|
+
let transcript = parseWhisperCppJson(rawJson);
|
|
3372
|
+
if (!options.reset) {
|
|
3373
|
+
const existing = readTranscript(project);
|
|
3374
|
+
if (existing) {
|
|
3375
|
+
transcript = mergeTranscriptWithExisting(transcript, existing);
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
const wordCount = transcript.segments.reduce((n, s) => n + s.words.length, 0);
|
|
3379
|
+
const result = {
|
|
3380
|
+
projectDir: project.dir,
|
|
3381
|
+
backend,
|
|
3382
|
+
transcript,
|
|
3383
|
+
wordCount,
|
|
3384
|
+
captionsGenerated: false
|
|
3385
|
+
};
|
|
3386
|
+
if (options.dryRun) return result;
|
|
3387
|
+
writeTranscript(project, transcript);
|
|
3388
|
+
if (!options.noCaptions) {
|
|
3389
|
+
const doc = readComposition(project);
|
|
3390
|
+
const updated = rewriteCaptionLayers(doc, transcript, options.captionOptions);
|
|
3391
|
+
writeComposition(project, updated);
|
|
3392
|
+
result.captionsGenerated = true;
|
|
3393
|
+
}
|
|
3394
|
+
return result;
|
|
3395
|
+
}
|
|
3396
|
+
function formatResult3(result) {
|
|
3397
|
+
const lines = [];
|
|
3398
|
+
lines.push(`Transcribed ${result.projectDir} via ${result.backend}`);
|
|
3399
|
+
if (result.transcript.language) {
|
|
3400
|
+
lines.push(` language: ${result.transcript.language}`);
|
|
3401
|
+
}
|
|
3402
|
+
lines.push(` segments: ${result.transcript.segments.length}`);
|
|
3403
|
+
lines.push(` words: ${result.wordCount}`);
|
|
3404
|
+
if (result.captionsGenerated) {
|
|
3405
|
+
lines.push(` captions: written to project.atelier`);
|
|
3406
|
+
} else {
|
|
3407
|
+
lines.push(` captions: skipped`);
|
|
3408
|
+
}
|
|
3409
|
+
return lines.join("\n");
|
|
3410
|
+
}
|
|
3411
|
+
function transcribeCommand(program2) {
|
|
3412
|
+
program2.command("transcribe <project>").description(
|
|
3413
|
+
"Transcribe source video via Whisper, write transcript.json, and rewrite caption-tagged TextVisual layers in project.atelier. Preserves user transcript edits on re-run."
|
|
3414
|
+
).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) => {
|
|
3415
|
+
try {
|
|
3416
|
+
let transcribeOpts = {
|
|
3417
|
+
model: opts.model,
|
|
3418
|
+
language: opts.language,
|
|
3419
|
+
reset: opts.reset,
|
|
3420
|
+
noCaptions: !opts.captions,
|
|
3421
|
+
dryRun: opts.dryRun
|
|
3422
|
+
};
|
|
3423
|
+
if (opts.recipe) {
|
|
3424
|
+
const { recipe } = loadRecipe(opts.recipe, project);
|
|
3425
|
+
transcribeOpts = applyRecipeToTranscribeOptions(recipe, transcribeOpts);
|
|
3426
|
+
}
|
|
3427
|
+
const result = await transcribeProject(project, transcribeOpts);
|
|
3428
|
+
if (opts.json) {
|
|
3429
|
+
console.log(JSON.stringify(result, null, 2));
|
|
3430
|
+
} else {
|
|
3431
|
+
console.log(formatResult3(result));
|
|
3432
|
+
if (opts.dryRun) console.log("(dry-run \u2014 no files written)");
|
|
3433
|
+
}
|
|
3434
|
+
} catch (err) {
|
|
3435
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3436
|
+
console.error(`atelier transcribe: ${msg}`);
|
|
3437
|
+
process.exit(1);
|
|
3438
|
+
}
|
|
3439
|
+
});
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3442
|
+
// src/commands/transcript.ts
|
|
3443
|
+
function loadOrThrow(projectDir) {
|
|
3444
|
+
const project = loadVideoProject(projectDir);
|
|
3445
|
+
const transcript = readTranscript(project);
|
|
3446
|
+
if (!transcript) {
|
|
3447
|
+
throw new Error(
|
|
3448
|
+
`No transcript.json in ${projectDir}. Run \`atelier transcribe ${projectDir}\` first.`
|
|
3449
|
+
);
|
|
3450
|
+
}
|
|
3451
|
+
return { project, transcript };
|
|
3452
|
+
}
|
|
3453
|
+
function save(project, transcript, noRegenerate) {
|
|
3454
|
+
writeTranscript(project, transcript);
|
|
3455
|
+
if (!noRegenerate) {
|
|
3456
|
+
const doc = readComposition(project);
|
|
3457
|
+
const updated = rewriteCaptionLayers(doc, transcript);
|
|
3458
|
+
writeComposition(project, updated);
|
|
3459
|
+
}
|
|
3460
|
+
}
|
|
3461
|
+
function transcriptCommand(program2) {
|
|
3462
|
+
const transcript = program2.command("transcript").description("Edit transcript.json \u2014 fix, add, delete, merge, split, list");
|
|
3463
|
+
transcript.command("fix <project>").description("Apply text correction(s). Use --replace 'wrong=right' for batch, or --word <idx> --text '<correction>' for single edit.").option("--replace <pair...>", "Batch find/replace, format: 'detected=replacement' (repeatable)").option("--word <index>", "Single-word edit by index", (v) => parseInt(v, 10)).option("--text <text>", "Correction text (paired with --word)").option("--no-regenerate", "Skip caption layer regeneration after edit").action(async (projectDir, opts) => {
|
|
3464
|
+
try {
|
|
3465
|
+
const { project, transcript: transcript2 } = loadOrThrow(projectDir);
|
|
3466
|
+
let updated = transcript2;
|
|
3467
|
+
if (opts.replace?.length) {
|
|
3468
|
+
for (const pair of opts.replace) {
|
|
3469
|
+
const [find, repl] = pair.split("=");
|
|
3470
|
+
if (!find || repl === void 0) {
|
|
3471
|
+
throw new Error(`Invalid --replace pair: "${pair}". Use 'detected=replacement'.`);
|
|
3472
|
+
}
|
|
3473
|
+
updated = applyBatchReplace(updated, find, repl);
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
if (typeof opts.word === "number") {
|
|
3477
|
+
if (opts.text === void 0) {
|
|
3478
|
+
throw new Error("--word requires --text <correction>");
|
|
3479
|
+
}
|
|
3480
|
+
updated = applyTextEdit(updated, opts.word, opts.text);
|
|
3481
|
+
}
|
|
3482
|
+
if (!opts.replace?.length && opts.word === void 0) {
|
|
3483
|
+
throw new Error("Provide --replace 'wrong=right' or --word <idx> --text '...'");
|
|
3484
|
+
}
|
|
3485
|
+
save(project, updated, !opts.regenerate);
|
|
3486
|
+
console.log(`Updated transcript in ${project.dir}`);
|
|
3487
|
+
} catch (err) {
|
|
3488
|
+
console.error(`atelier transcript fix: ${err instanceof Error ? err.message : err}`);
|
|
3489
|
+
process.exit(1);
|
|
3490
|
+
}
|
|
3491
|
+
});
|
|
3492
|
+
transcript.command("add <project>").description("Insert a user-added word after the anchor word.").requiredOption("--after-word <index>", "Anchor word index", (v) => parseInt(v, 10)).requiredOption("--text <text>", "Text of the new word").option("--duration <seconds>", "Word duration in seconds (default: 0.15)", (v) => parseFloat(v), 0.15).option("--no-regenerate", "Skip caption layer regeneration").action(async (projectDir, opts) => {
|
|
3493
|
+
try {
|
|
3494
|
+
const { project, transcript: transcript2 } = loadOrThrow(projectDir);
|
|
3495
|
+
const updated = applyAdd(transcript2, opts.afterWord, opts.text, opts.duration);
|
|
3496
|
+
save(project, updated, !opts.regenerate);
|
|
3497
|
+
console.log(`Inserted "${opts.text}" after word ${opts.afterWord} in ${project.dir}`);
|
|
3498
|
+
} catch (err) {
|
|
3499
|
+
console.error(`atelier transcript add: ${err instanceof Error ? err.message : err}`);
|
|
3500
|
+
process.exit(1);
|
|
3501
|
+
}
|
|
3502
|
+
});
|
|
3503
|
+
transcript.command("delete <project>").description("Hide a word (excluded from captions, kept in transcript).").requiredOption("--word <index>", "Word index", (v) => parseInt(v, 10)).option("--no-regenerate", "Skip caption layer regeneration").action(async (projectDir, opts) => {
|
|
3504
|
+
try {
|
|
3505
|
+
const { project, transcript: transcript2 } = loadOrThrow(projectDir);
|
|
3506
|
+
const updated = applyHide(transcript2, opts.word);
|
|
3507
|
+
save(project, updated, !opts.regenerate);
|
|
3508
|
+
console.log(`Hidden word ${opts.word} in ${project.dir}`);
|
|
3509
|
+
} catch (err) {
|
|
3510
|
+
console.error(`atelier transcript delete: ${err instanceof Error ? err.message : err}`);
|
|
3511
|
+
process.exit(1);
|
|
3512
|
+
}
|
|
3513
|
+
});
|
|
3514
|
+
transcript.command("merge <project>").description("Merge two adjacent words at indices i and i+1.").requiredOption("--word <index>", "First of the two words to merge", (v) => parseInt(v, 10)).option("--no-regenerate", "Skip caption layer regeneration").action(async (projectDir, opts) => {
|
|
3515
|
+
try {
|
|
3516
|
+
const { project, transcript: transcript2 } = loadOrThrow(projectDir);
|
|
3517
|
+
const updated = applyMerge(transcript2, opts.word);
|
|
3518
|
+
save(project, updated, !opts.regenerate);
|
|
3519
|
+
console.log(`Merged words ${opts.word} and ${opts.word + 1} in ${project.dir}`);
|
|
3520
|
+
} catch (err) {
|
|
3521
|
+
console.error(`atelier transcript merge: ${err instanceof Error ? err.message : err}`);
|
|
3522
|
+
process.exit(1);
|
|
3523
|
+
}
|
|
3524
|
+
});
|
|
3525
|
+
transcript.command("split <project>").description("Split one word at a fractional point.").requiredOption("--word <index>", "Word to split", (v) => parseInt(v, 10)).requiredOption("--at <fraction>", "Split point (0\u20131)", (v) => parseFloat(v)).option("--first <text>", "Override text for first half").option("--second <text>", "Override text for second half").option("--no-regenerate", "Skip caption layer regeneration").action(async (projectDir, opts) => {
|
|
3526
|
+
try {
|
|
3527
|
+
const { project, transcript: transcript2 } = loadOrThrow(projectDir);
|
|
3528
|
+
const updated = applySplit(transcript2, opts.word, opts.at, opts.first, opts.second);
|
|
3529
|
+
save(project, updated, !opts.regenerate);
|
|
3530
|
+
console.log(`Split word ${opts.word} at ${opts.at} in ${project.dir}`);
|
|
3531
|
+
} catch (err) {
|
|
3532
|
+
console.error(`atelier transcript split: ${err instanceof Error ? err.message : err}`);
|
|
3533
|
+
process.exit(1);
|
|
3534
|
+
}
|
|
3535
|
+
});
|
|
3536
|
+
transcript.command("list <project>").description("Print all words with their indices for reference.").option("--json", "Output as JSON").action(async (projectDir, opts) => {
|
|
3537
|
+
try {
|
|
3538
|
+
const { transcript: transcript2 } = loadOrThrow(projectDir);
|
|
3539
|
+
const words = flattenWords(transcript2);
|
|
3540
|
+
if (opts.json) {
|
|
3541
|
+
console.log(JSON.stringify(words.map((w, i) => ({ index: i, ...w })), null, 2));
|
|
3542
|
+
} else {
|
|
3543
|
+
for (let i = 0; i < words.length; i++) {
|
|
3544
|
+
const w = words[i];
|
|
3545
|
+
const flags = [];
|
|
3546
|
+
if (w.userEdited) flags.push("edited");
|
|
3547
|
+
if (w.userAdded) flags.push("added");
|
|
3548
|
+
if (w.hidden) flags.push("hidden");
|
|
3549
|
+
const flagStr = flags.length ? ` [${flags.join(", ")}]` : "";
|
|
3550
|
+
const editedDisplay = w.text !== w.detected ? ` \u2190 "${w.detected}"` : "";
|
|
3551
|
+
console.log(
|
|
3552
|
+
` [${i.toString().padStart(4)}] ${w.start.toFixed(2).padStart(7)}s "${w.text}"${editedDisplay}${flagStr}`
|
|
3553
|
+
);
|
|
3554
|
+
}
|
|
3555
|
+
}
|
|
3556
|
+
} catch (err) {
|
|
3557
|
+
console.error(`atelier transcript list: ${err instanceof Error ? err.message : err}`);
|
|
3558
|
+
process.exit(1);
|
|
3559
|
+
}
|
|
3560
|
+
});
|
|
3561
|
+
}
|
|
3562
|
+
|
|
3563
|
+
// src/commands/captions.ts
|
|
3564
|
+
function captionsCommand(program2) {
|
|
3565
|
+
const captions = program2.command("captions").description("Manage caption layers in a VideoProject");
|
|
3566
|
+
captions.command("regenerate <project>").description(
|
|
3567
|
+
"Re-derive caption-tagged TextVisual layers from the current transcript.json. Used when caption styling changes without re-running Whisper."
|
|
3568
|
+
).option("--recipe <name>", "Apply a Studio Recipe's caption_style + caption_grouping").action(async (projectDir, opts) => {
|
|
3569
|
+
try {
|
|
3570
|
+
const project = loadVideoProject(projectDir);
|
|
3571
|
+
const transcript = readTranscript(project);
|
|
3572
|
+
if (!transcript) {
|
|
3573
|
+
throw new Error(
|
|
3574
|
+
`No transcript.json in ${projectDir}. Run \`atelier transcribe ${projectDir}\` first.`
|
|
3575
|
+
);
|
|
3576
|
+
}
|
|
3577
|
+
const captionOptions = opts.recipe ? applyRecipeToCaptionOptions(loadRecipe(opts.recipe, projectDir).recipe) : {};
|
|
3578
|
+
const doc = readComposition(project);
|
|
3579
|
+
const updated = rewriteCaptionLayers(doc, transcript, captionOptions);
|
|
3580
|
+
writeComposition(project, updated);
|
|
3581
|
+
const captionLayers = updated.layers.filter(
|
|
3582
|
+
(l) => (l.tags ?? []).includes("caption")
|
|
3583
|
+
);
|
|
3584
|
+
console.log(
|
|
3585
|
+
`Regenerated ${captionLayers.length} caption layer${captionLayers.length === 1 ? "" : "s"} in ${project.dir}`
|
|
3586
|
+
);
|
|
3587
|
+
} catch (err) {
|
|
3588
|
+
console.error(`atelier captions regenerate: ${err instanceof Error ? err.message : err}`);
|
|
3589
|
+
process.exit(1);
|
|
3590
|
+
}
|
|
3591
|
+
});
|
|
3592
|
+
}
|
|
3593
|
+
|
|
3594
|
+
// src/commands/recipe.ts
|
|
3595
|
+
var import_node_fs6 = require("fs");
|
|
3596
|
+
var import_node_path5 = require("path");
|
|
3597
|
+
function recipeCommand(program2) {
|
|
3598
|
+
const recipe = program2.command("recipe").description("Manage Studio Recipes \u2014 reusable style presets");
|
|
3599
|
+
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) => {
|
|
3600
|
+
try {
|
|
3601
|
+
const baseDir = opts.dir ?? (0, import_node_path5.join)((0, import_node_path5.resolve)(process.cwd()), ".atelier", "recipes");
|
|
3602
|
+
if (!(0, import_node_fs6.existsSync)(baseDir)) {
|
|
3603
|
+
(0, import_node_fs6.mkdirSync)(baseDir, { recursive: true });
|
|
3604
|
+
}
|
|
3605
|
+
const hasKnownExt = /\.(recipe\.yaml|recipe\.json|yaml|yml|json)$/i.test(name);
|
|
3606
|
+
const fileName = hasKnownExt ? name : `${name}.recipe.yaml`;
|
|
3607
|
+
const outPath = (0, import_node_path5.join)(baseDir, fileName);
|
|
3608
|
+
if ((0, import_node_fs6.existsSync)(outPath)) {
|
|
3609
|
+
throw new Error(`Recipe already exists at ${outPath} \u2014 refusing to overwrite.`);
|
|
3610
|
+
}
|
|
3611
|
+
const recipeName = name.replace(/\.(recipe\.yaml|recipe\.json|yaml|yml|json)$/i, "");
|
|
3612
|
+
const yaml = scaffoldRecipeYaml(recipeName);
|
|
3613
|
+
(0, import_node_fs6.writeFileSync)(outPath, yaml, "utf-8");
|
|
3614
|
+
console.log(`Created ${outPath}`);
|
|
3615
|
+
} catch (err) {
|
|
3616
|
+
console.error(`atelier recipe new: ${err instanceof Error ? err.message : err}`);
|
|
3617
|
+
process.exit(1);
|
|
3618
|
+
}
|
|
3619
|
+
});
|
|
3620
|
+
recipe.command("validate <path>").description("Validate a recipe against the schema (^valid-recipe gate). Warns on reserved Phase 3 fields.").option("--json", "Output result as JSON").action((path, opts) => {
|
|
3621
|
+
try {
|
|
3622
|
+
const loaded = loadRecipe(path, process.cwd());
|
|
3623
|
+
if (opts.json) {
|
|
3624
|
+
console.log(JSON.stringify({
|
|
3625
|
+
valid: true,
|
|
3626
|
+
path: loaded.path,
|
|
3627
|
+
warnings: loaded.warnings
|
|
3628
|
+
}, null, 2));
|
|
3629
|
+
} else {
|
|
3630
|
+
console.log(`PASS ${loaded.path}`);
|
|
3631
|
+
for (const w of loaded.warnings) {
|
|
3632
|
+
console.log(` \u26A0 ${w}`);
|
|
3633
|
+
}
|
|
3634
|
+
}
|
|
3635
|
+
} catch (err) {
|
|
3636
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
3637
|
+
if (opts.json) {
|
|
3638
|
+
console.log(JSON.stringify({ valid: false, error: msg }, null, 2));
|
|
3639
|
+
} else {
|
|
3640
|
+
console.error(`FAIL ${path}`);
|
|
3641
|
+
console.error(` ${msg}`);
|
|
3642
|
+
}
|
|
3643
|
+
process.exit(1);
|
|
3644
|
+
}
|
|
3645
|
+
});
|
|
3646
|
+
recipe.command("show <path>").description("Print a recipe; --with-defaults fills in every omitted field with its code default.").option("--with-defaults", "Overlay code defaults onto omitted fields").action((path, opts) => {
|
|
3647
|
+
try {
|
|
3648
|
+
const loaded = loadRecipe(path, process.cwd());
|
|
3649
|
+
const out = opts.withDefaults ? renderRecipeWithDefaults(loaded.recipe) : loaded.recipe;
|
|
3650
|
+
console.log(recipeToYaml(out));
|
|
3651
|
+
for (const w of loaded.warnings) {
|
|
3652
|
+
console.error(`# \u26A0 ${w}`);
|
|
3653
|
+
}
|
|
3654
|
+
} catch (err) {
|
|
3655
|
+
console.error(`atelier recipe show: ${err instanceof Error ? err.message : err}`);
|
|
3656
|
+
process.exit(1);
|
|
3657
|
+
}
|
|
3658
|
+
});
|
|
3659
|
+
}
|
|
3660
|
+
|
|
3661
|
+
// src/commands/apply-recipe.ts
|
|
3662
|
+
function applyRecipeCommand(program2) {
|
|
3663
|
+
program2.command("apply-recipe <project> <recipe>").description(
|
|
3664
|
+
"Apply a Studio Recipe by running atelier trim + atelier transcribe with the same recipe, in that order."
|
|
3665
|
+
).option("--reset", "Apply --reset to both pipelines (destructive \u2014 discards existing user padding and transcript edits)").option("--no-trim", "Skip the atelier trim step").option("--no-transcribe", "Skip the atelier transcribe step").action(async (project, recipeRef, opts) => {
|
|
3666
|
+
try {
|
|
3667
|
+
const { recipe, path, warnings } = loadRecipe(recipeRef, project);
|
|
3668
|
+
console.log(`Loaded recipe ${path}`);
|
|
3669
|
+
for (const w of warnings) console.log(` \u26A0 ${w}`);
|
|
3670
|
+
if (opts.trim) {
|
|
3671
|
+
const trimOpts = applyRecipeToTrimOptions(recipe, { reset: opts.reset });
|
|
3672
|
+
console.log(`
|
|
3673
|
+
Running atelier trim...`);
|
|
3674
|
+
const r = await trimProject(project, trimOpts);
|
|
3675
|
+
console.log(` ${r.cuts.length} cut${r.cuts.length === 1 ? "" : "s"} written`);
|
|
3676
|
+
}
|
|
3677
|
+
if (opts.transcribe) {
|
|
3678
|
+
const transcribeOpts = applyRecipeToTranscribeOptions(recipe, { reset: opts.reset });
|
|
3679
|
+
console.log(`
|
|
3680
|
+
Running atelier transcribe...`);
|
|
3681
|
+
const r = await transcribeProject(project, transcribeOpts);
|
|
3682
|
+
console.log(` ${r.wordCount} words, ${r.captionsGenerated ? "captions written" : "captions skipped"}`);
|
|
3683
|
+
}
|
|
3684
|
+
if (recipe.overlay_rules) {
|
|
3685
|
+
const vp = loadVideoProject(project);
|
|
3686
|
+
const doc = readComposition(vp);
|
|
3687
|
+
const updated = applyRecipeToOverlay(doc, recipe);
|
|
3688
|
+
writeComposition(vp, updated);
|
|
3689
|
+
console.log(`
|
|
3690
|
+
Applied overlay rules to ${vp.compositionPath}`);
|
|
3691
|
+
}
|
|
3692
|
+
console.log(`
|
|
3693
|
+
Done.`);
|
|
3694
|
+
} catch (err) {
|
|
3695
|
+
console.error(`atelier apply-recipe: ${err instanceof Error ? err.message : err}`);
|
|
3696
|
+
process.exit(1);
|
|
3697
|
+
}
|
|
3698
|
+
});
|
|
3699
|
+
}
|
|
3700
|
+
|
|
3701
|
+
// src/commands/carousel.ts
|
|
3702
|
+
var import_node_fs7 = require("fs");
|
|
3703
|
+
var import_node_path6 = require("path");
|
|
3704
|
+
|
|
3705
|
+
// src/lib/render-image.ts
|
|
3706
|
+
init_dist2();
|
|
3707
|
+
var import_canvas = require("@napi-rs/canvas");
|
|
3708
|
+
function fitImageToCanvas(canvas, natural) {
|
|
3709
|
+
const cw = canvas.width;
|
|
3710
|
+
const ch = canvas.height;
|
|
3711
|
+
const iw = natural.width;
|
|
3712
|
+
const ih = natural.height;
|
|
3713
|
+
if (!iw || !ih) {
|
|
3714
|
+
return { bounds: { width: cw, height: ch }, frame: { x: cw / 2, y: ch / 2 } };
|
|
3715
|
+
}
|
|
3716
|
+
const imageAspect = iw / ih;
|
|
3717
|
+
const canvasAspect = cw / ch;
|
|
3718
|
+
let width;
|
|
3719
|
+
let height;
|
|
3720
|
+
if (imageAspect > canvasAspect) {
|
|
3721
|
+
width = cw;
|
|
3722
|
+
height = cw / imageAspect;
|
|
3723
|
+
} else {
|
|
3724
|
+
height = ch;
|
|
3725
|
+
width = ch * imageAspect;
|
|
3726
|
+
}
|
|
3727
|
+
width = Math.min(width, cw);
|
|
3728
|
+
height = Math.min(height, ch);
|
|
3729
|
+
return { bounds: { width, height }, frame: { x: cw / 2, y: ch / 2 } };
|
|
3730
|
+
}
|
|
3731
|
+
var CanvasUnavailableError = class extends Error {
|
|
3732
|
+
constructor() {
|
|
3733
|
+
super(
|
|
3734
|
+
"The '@napi-rs/canvas' rasterizer could not be loaded.\nThis package ships prebuilt platform binaries \u2014 no system libraries needed.\nTry reinstalling dependencies (e.g. `npm install`) to fetch the binary for this platform."
|
|
3735
|
+
);
|
|
3736
|
+
this.name = "CanvasUnavailableError";
|
|
3737
|
+
}
|
|
3738
|
+
};
|
|
3739
|
+
async function loadCanvasModule() {
|
|
3740
|
+
if (typeof import_canvas.createCanvas !== "function" || typeof import_canvas.loadImage !== "function") {
|
|
3741
|
+
throw new CanvasUnavailableError();
|
|
3742
|
+
}
|
|
3743
|
+
return {
|
|
3744
|
+
createCanvas: import_canvas.createCanvas,
|
|
3745
|
+
loadImage: import_canvas.loadImage
|
|
3746
|
+
};
|
|
3747
|
+
}
|
|
3748
|
+
function resolveExportDimensions(docWidth, docHeight, width, height) {
|
|
3749
|
+
if (width !== void 0 && height !== void 0) {
|
|
3750
|
+
return { width, height };
|
|
3751
|
+
}
|
|
3752
|
+
if (width !== void 0) {
|
|
3753
|
+
const h = Math.max(1, Math.round(width * docHeight / docWidth));
|
|
3754
|
+
return { width, height: h };
|
|
3755
|
+
}
|
|
3756
|
+
if (height !== void 0) {
|
|
3757
|
+
const w = Math.max(1, Math.round(height * docWidth / docHeight));
|
|
3758
|
+
return { width: w, height };
|
|
3759
|
+
}
|
|
3760
|
+
return { width: docWidth, height: docHeight };
|
|
3761
|
+
}
|
|
3762
|
+
async function preloadImages(doc, loadImage2) {
|
|
3763
|
+
const { ImageCache: ImageCache2 } = await Promise.resolve().then(() => (init_dist3(), dist_exports));
|
|
3764
|
+
const sources = /* @__PURE__ */ new Set();
|
|
3765
|
+
for (const layer of doc.layers) {
|
|
3766
|
+
if (layer.visual.type === "image") {
|
|
3767
|
+
const iv = layer.visual;
|
|
3768
|
+
if (iv.src) sources.add(iv.src);
|
|
3769
|
+
else if (iv.assetId && doc.assets?.[iv.assetId]) sources.add(doc.assets[iv.assetId].src);
|
|
3770
|
+
}
|
|
3771
|
+
}
|
|
3772
|
+
if (sources.size === 0) {
|
|
3773
|
+
return { imageCache: new ImageCache2(), loaded: /* @__PURE__ */ new Map() };
|
|
3774
|
+
}
|
|
3775
|
+
const preloaded = /* @__PURE__ */ new Map();
|
|
3776
|
+
await Promise.all(
|
|
3777
|
+
[...sources].map(async (src) => {
|
|
3778
|
+
try {
|
|
3779
|
+
preloaded.set(src, await loadImage2(src));
|
|
3780
|
+
} catch {
|
|
3781
|
+
}
|
|
3782
|
+
})
|
|
3783
|
+
);
|
|
3784
|
+
const imageCache = new ImageCache2({
|
|
3785
|
+
createImage: (src, onLoad, onError) => {
|
|
3786
|
+
const img = preloaded.get(src);
|
|
3787
|
+
if (img) {
|
|
3788
|
+
process.nextTick(onLoad);
|
|
3789
|
+
return img;
|
|
3790
|
+
}
|
|
3791
|
+
process.nextTick(onError);
|
|
3792
|
+
return {};
|
|
3793
|
+
}
|
|
3794
|
+
});
|
|
3795
|
+
for (const src of preloaded.keys()) imageCache.load(src);
|
|
3796
|
+
await new Promise((resolve16) => process.nextTick(resolve16));
|
|
3797
|
+
return { imageCache, loaded: preloaded };
|
|
3798
|
+
}
|
|
3799
|
+
async function renderDocumentToPng(doc, opts = {}) {
|
|
3800
|
+
const stateNames = Object.keys(doc.states);
|
|
3801
|
+
if (stateNames.length === 0) {
|
|
3802
|
+
throw new Error("Document has no states");
|
|
3803
|
+
}
|
|
3804
|
+
const stateName = opts.state ?? stateNames[0];
|
|
3805
|
+
if (!doc.states[stateName]) {
|
|
3806
|
+
throw new Error(`State "${stateName}" not found. Available: ${stateNames.join(", ")}`);
|
|
3807
|
+
}
|
|
3808
|
+
const frameNumber = opts.frame ?? 0;
|
|
3809
|
+
if (!Number.isInteger(frameNumber) || frameNumber < 0) {
|
|
3810
|
+
throw new Error(`Invalid frame number: ${frameNumber}`);
|
|
3811
|
+
}
|
|
3812
|
+
const duration = doc.states[stateName].duration;
|
|
3813
|
+
if (frameNumber >= duration) {
|
|
3814
|
+
throw new Error(
|
|
3815
|
+
`Frame ${frameNumber} is out of range for state "${stateName}" (duration ${duration})`
|
|
3816
|
+
);
|
|
3817
|
+
}
|
|
3818
|
+
const { createCanvas: createCanvas2, loadImage: loadImage2 } = await loadCanvasModule();
|
|
3819
|
+
const { renderFrame: renderFrame2 } = await Promise.resolve().then(() => (init_dist3(), dist_exports));
|
|
3820
|
+
const { imageCache, loaded } = await preloadImages(doc, loadImage2);
|
|
3821
|
+
let renderDoc = doc;
|
|
3822
|
+
if (opts.refitImageBounds && loaded.size > 0) {
|
|
3823
|
+
renderDoc = {
|
|
3824
|
+
...doc,
|
|
3825
|
+
layers: doc.layers.map((layer) => {
|
|
3826
|
+
if (layer.visual.type !== "image") return layer;
|
|
3827
|
+
const iv = layer.visual;
|
|
3828
|
+
const src = iv.src ?? (iv.assetId ? doc.assets?.[iv.assetId]?.src : void 0);
|
|
3829
|
+
const natural = src ? loaded.get(src) : void 0;
|
|
3830
|
+
if (!natural || !natural.width || !natural.height) return layer;
|
|
3831
|
+
const fit = opts.refitImageBounds({ canvas: doc.canvas, natural });
|
|
3832
|
+
return { ...layer, bounds: fit.bounds, frame: fit.frame };
|
|
3833
|
+
})
|
|
3834
|
+
};
|
|
3835
|
+
}
|
|
3836
|
+
const { width: outW, height: outH } = resolveExportDimensions(
|
|
3837
|
+
renderDoc.canvas.width,
|
|
3838
|
+
renderDoc.canvas.height,
|
|
3839
|
+
opts.width,
|
|
3840
|
+
opts.height
|
|
3841
|
+
);
|
|
3842
|
+
const resolved = resolveFrame(renderDoc, stateName, frameNumber);
|
|
3843
|
+
const cvs = createCanvas2(outW, outH);
|
|
3844
|
+
const ctx = cvs.getContext("2d");
|
|
3845
|
+
const sx = outW / renderDoc.canvas.width;
|
|
3846
|
+
const sy = outH / renderDoc.canvas.height;
|
|
3847
|
+
if (sx !== 1 || sy !== 1) ctx.scale(sx, sy);
|
|
3848
|
+
renderFrame2(ctx, resolved, renderDoc, imageCache);
|
|
3849
|
+
return cvs.toBuffer("image/png");
|
|
3850
|
+
}
|
|
3851
|
+
|
|
3852
|
+
// src/commands/carousel.ts
|
|
3853
|
+
var IMAGE_EXTS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".webp"]);
|
|
3854
|
+
var DEFAULT_CANVAS = 1080;
|
|
3855
|
+
function expandInputs(pattern) {
|
|
3856
|
+
const abs = (0, import_node_path6.resolve)(pattern);
|
|
3857
|
+
let isDir = false;
|
|
3858
|
+
try {
|
|
3859
|
+
isDir = (0, import_node_fs7.statSync)(abs).isDirectory();
|
|
3860
|
+
} catch {
|
|
3861
|
+
isDir = false;
|
|
3862
|
+
}
|
|
3863
|
+
if (isDir) {
|
|
3864
|
+
return listImages(abs);
|
|
3865
|
+
}
|
|
3866
|
+
const dir = (0, import_node_path6.dirname)(abs);
|
|
3867
|
+
const base = (0, import_node_path6.basename)(abs);
|
|
3868
|
+
if (base.includes("*")) {
|
|
3869
|
+
const matcher = globToRegExp(base);
|
|
3870
|
+
let entries;
|
|
3871
|
+
try {
|
|
3872
|
+
entries = (0, import_node_fs7.readdirSync)(dir);
|
|
3873
|
+
} catch {
|
|
3874
|
+
return [];
|
|
3875
|
+
}
|
|
3876
|
+
return entries.filter((name) => matcher.test(name) && isImageFile(name)).map((name) => (0, import_node_path6.join)(dir, name)).sort();
|
|
1862
3877
|
}
|
|
1863
|
-
return
|
|
3878
|
+
return isImageFile(base) ? [abs] : [];
|
|
1864
3879
|
}
|
|
1865
|
-
function
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
3880
|
+
function listImages(dir) {
|
|
3881
|
+
let entries;
|
|
3882
|
+
try {
|
|
3883
|
+
entries = (0, import_node_fs7.readdirSync)(dir);
|
|
3884
|
+
} catch {
|
|
3885
|
+
return [];
|
|
3886
|
+
}
|
|
3887
|
+
return entries.filter(isImageFile).map((name) => (0, import_node_path6.join)(dir, name)).sort();
|
|
3888
|
+
}
|
|
3889
|
+
function isImageFile(name) {
|
|
3890
|
+
return IMAGE_EXTS.has((0, import_node_path6.extname)(name).toLowerCase());
|
|
3891
|
+
}
|
|
3892
|
+
function globToRegExp(glob) {
|
|
3893
|
+
const escaped = glob.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
3894
|
+
const pattern = escaped.replace(/\*/g, `[^${import_node_path6.sep === "\\" ? "\\\\" : import_node_path6.sep}]*`).replace(/\?/g, ".");
|
|
3895
|
+
return new RegExp(`^${pattern}$`);
|
|
3896
|
+
}
|
|
3897
|
+
function composeCarouselFrameDoc(args) {
|
|
3898
|
+
const { imagePath, index, total, width, height, recipe } = args;
|
|
3899
|
+
const canvas = { width, height };
|
|
3900
|
+
const fit = fitImageToCanvas(canvas, { width: 0, height: 0 });
|
|
3901
|
+
const assetId = "carousel-image-asset";
|
|
3902
|
+
const baseDoc = {
|
|
3903
|
+
version: "1.0",
|
|
3904
|
+
name: `carousel-${index}`,
|
|
3905
|
+
canvas: { width, height, fps: 30, background: args.background ?? "#000000" },
|
|
3906
|
+
assets: { [assetId]: { type: "image", src: imagePath } },
|
|
3907
|
+
layers: [
|
|
3908
|
+
{
|
|
3909
|
+
id: "carousel-image",
|
|
3910
|
+
visual: { type: "image", assetId, src: imagePath },
|
|
3911
|
+
frame: fit.frame,
|
|
3912
|
+
bounds: fit.bounds,
|
|
3913
|
+
anchorPoint: { x: 0.5, y: 0.5 },
|
|
3914
|
+
opacity: 1
|
|
3915
|
+
}
|
|
3916
|
+
],
|
|
3917
|
+
states: { default: { duration: 1, deltas: [] } }
|
|
3918
|
+
};
|
|
3919
|
+
return applyRecipeToOverlay(baseDoc, recipe, { currentIndex: index, totalCount: total });
|
|
3920
|
+
}
|
|
3921
|
+
function carouselFileName(index, total, imagePath) {
|
|
3922
|
+
const padWidth = Math.max(2, String(total).length);
|
|
3923
|
+
const prefix = String(index).padStart(padWidth, "0");
|
|
3924
|
+
const ext = (0, import_node_path6.extname)(imagePath);
|
|
3925
|
+
const stem = (0, import_node_path6.basename)(imagePath, ext);
|
|
3926
|
+
return `${prefix}-${stem}.png`;
|
|
3927
|
+
}
|
|
3928
|
+
function parseDim(raw, name) {
|
|
3929
|
+
if (raw === void 0) return void 0;
|
|
3930
|
+
const n = parseInt(raw, 10);
|
|
3931
|
+
if (isNaN(n) || n <= 0) {
|
|
3932
|
+
console.error(`Invalid --${name}: ${raw}`);
|
|
3933
|
+
process.exit(1);
|
|
3934
|
+
}
|
|
3935
|
+
return n;
|
|
3936
|
+
}
|
|
3937
|
+
function carouselCommand(program2) {
|
|
3938
|
+
program2.command("carousel <recipe>").description(
|
|
3939
|
+
"Batch-compose a folder of images into recipe-overlaid PNGs. Each image becomes a fit-to-canvas post with the recipe's handle + page-number ('i/N') overlays, written to --out-dir as zero-padded PNGs."
|
|
3940
|
+
).requiredOption("-i, --inputs <glob>", "Input images: a directory, file, or single-segment *-glob").requiredOption("-d, --out-dir <dir>", "Destination directory for composed PNGs").option("--width <number>", "Canvas width (px)", String(DEFAULT_CANVAS)).option("--height <number>", "Canvas height (px)", String(DEFAULT_CANVAS)).option("-f, --frame <number>", "Frame to render (defaults to 0)", "0").action(async (recipeRef, options) => {
|
|
3941
|
+
let recipe;
|
|
3942
|
+
try {
|
|
3943
|
+
const loaded = loadRecipe(recipeRef);
|
|
3944
|
+
recipe = loaded.recipe;
|
|
3945
|
+
console.log(`Loaded recipe ${loaded.path}`);
|
|
3946
|
+
for (const w of loaded.warnings) console.log(` \u26A0 ${w}`);
|
|
3947
|
+
} catch (err) {
|
|
3948
|
+
console.error(`atelier carousel: ${err instanceof Error ? err.message : err}`);
|
|
3949
|
+
process.exit(1);
|
|
3950
|
+
return;
|
|
3951
|
+
}
|
|
3952
|
+
const inputs = expandInputs(options.inputs);
|
|
3953
|
+
if (inputs.length === 0) {
|
|
3954
|
+
console.error(
|
|
3955
|
+
`atelier carousel: no image files matched --inputs "${options.inputs}" (accepted: ${[...IMAGE_EXTS].join(", ")})`
|
|
3956
|
+
);
|
|
3957
|
+
process.exit(1);
|
|
3958
|
+
return;
|
|
3959
|
+
}
|
|
3960
|
+
const width = parseDim(options.width, "width") ?? DEFAULT_CANVAS;
|
|
3961
|
+
const height = parseDim(options.height, "height") ?? DEFAULT_CANVAS;
|
|
3962
|
+
const frame = parseInt(options.frame, 10);
|
|
3963
|
+
if (isNaN(frame) || frame < 0) {
|
|
3964
|
+
console.error(`Invalid frame number: ${options.frame}`);
|
|
3965
|
+
process.exit(1);
|
|
3966
|
+
return;
|
|
3967
|
+
}
|
|
3968
|
+
const outDir = (0, import_node_path6.resolve)(options.outDir);
|
|
3969
|
+
try {
|
|
3970
|
+
(0, import_node_fs7.mkdirSync)(outDir, { recursive: true });
|
|
3971
|
+
} catch (err) {
|
|
3972
|
+
console.error(`atelier carousel: cannot create out-dir ${outDir}: ${err.message}`);
|
|
3973
|
+
process.exit(1);
|
|
3974
|
+
return;
|
|
3975
|
+
}
|
|
3976
|
+
const total = inputs.length;
|
|
3977
|
+
console.log(`Composing ${total} image${total === 1 ? "" : "s"} \u2192 ${outDir}`);
|
|
3978
|
+
try {
|
|
3979
|
+
for (let n = 0; n < total; n++) {
|
|
3980
|
+
const index = n + 1;
|
|
3981
|
+
const imagePath = inputs[n];
|
|
3982
|
+
const doc = composeCarouselFrameDoc({
|
|
3983
|
+
imagePath,
|
|
3984
|
+
index,
|
|
3985
|
+
total,
|
|
3986
|
+
width,
|
|
3987
|
+
height,
|
|
3988
|
+
recipe
|
|
3989
|
+
});
|
|
3990
|
+
const buffer = await renderDocumentToPng(doc, {
|
|
3991
|
+
frame,
|
|
3992
|
+
// Re-fit the image to the canvas using real natural dims once decoded.
|
|
3993
|
+
refitImageBounds: ({ canvas, natural }) => fitImageToCanvas(canvas, natural)
|
|
3994
|
+
});
|
|
3995
|
+
const outName = carouselFileName(index, total, imagePath);
|
|
3996
|
+
(0, import_node_fs7.writeFileSync)((0, import_node_path6.join)(outDir, outName), buffer);
|
|
3997
|
+
console.log(` [${index}/${total}] ${(0, import_node_path6.basename)(imagePath)} \u2192 ${outName}`);
|
|
3998
|
+
}
|
|
3999
|
+
} catch (err) {
|
|
4000
|
+
if (err instanceof CanvasUnavailableError) {
|
|
4001
|
+
console.error(err.message);
|
|
4002
|
+
process.exit(1);
|
|
4003
|
+
return;
|
|
1874
4004
|
}
|
|
4005
|
+
console.error(`atelier carousel: ${err instanceof Error ? err.message : err}`);
|
|
1875
4006
|
process.exit(1);
|
|
4007
|
+
return;
|
|
1876
4008
|
}
|
|
4009
|
+
console.log(`
|
|
4010
|
+
Done. ${total} image${total === 1 ? "" : "s"} \u2192 ${outDir}`);
|
|
1877
4011
|
});
|
|
1878
4012
|
}
|
|
1879
4013
|
|
|
1880
4014
|
// src/commands/info.ts
|
|
1881
|
-
var
|
|
1882
|
-
var
|
|
4015
|
+
var import_node_fs8 = require("fs");
|
|
4016
|
+
var import_node_path7 = require("path");
|
|
1883
4017
|
function getInfo(doc) {
|
|
1884
4018
|
return {
|
|
1885
4019
|
name: doc.name,
|
|
@@ -1936,10 +4070,10 @@ function formatInfo(info) {
|
|
|
1936
4070
|
return lines.join("\n");
|
|
1937
4071
|
}
|
|
1938
4072
|
function readAndParse(file) {
|
|
1939
|
-
const absPath = (0,
|
|
4073
|
+
const absPath = (0, import_node_path7.resolve)(file);
|
|
1940
4074
|
let content;
|
|
1941
4075
|
try {
|
|
1942
|
-
content = (0,
|
|
4076
|
+
content = (0, import_node_fs8.readFileSync)(absPath, "utf-8");
|
|
1943
4077
|
} catch {
|
|
1944
4078
|
console.error(`Cannot read file: ${absPath}`);
|
|
1945
4079
|
return process.exit(1);
|
|
@@ -1963,8 +4097,8 @@ function infoCommand(program2) {
|
|
|
1963
4097
|
}
|
|
1964
4098
|
|
|
1965
4099
|
// src/commands/still.ts
|
|
1966
|
-
var
|
|
1967
|
-
var
|
|
4100
|
+
var import_node_fs9 = require("fs");
|
|
4101
|
+
var import_node_path8 = require("path");
|
|
1968
4102
|
init_dist2();
|
|
1969
4103
|
function resolveStill(doc, stateName, frame) {
|
|
1970
4104
|
const stateNames = Object.keys(doc.states);
|
|
@@ -1981,10 +4115,10 @@ function resolveStill(doc, stateName, frame) {
|
|
|
1981
4115
|
return resolveFrame(doc, resolvedStateName, resolvedFrame);
|
|
1982
4116
|
}
|
|
1983
4117
|
function readAndParse2(file) {
|
|
1984
|
-
const absPath = (0,
|
|
4118
|
+
const absPath = (0, import_node_path8.resolve)(file);
|
|
1985
4119
|
let content;
|
|
1986
4120
|
try {
|
|
1987
|
-
content = (0,
|
|
4121
|
+
content = (0, import_node_fs9.readFileSync)(absPath, "utf-8");
|
|
1988
4122
|
} catch {
|
|
1989
4123
|
console.error(`Cannot read file: ${absPath}`);
|
|
1990
4124
|
return process.exit(1);
|
|
@@ -2035,27 +4169,19 @@ function stillCommand(program2) {
|
|
|
2035
4169
|
if (options.format === "json") {
|
|
2036
4170
|
const json = JSON.stringify(resolved, null, 2);
|
|
2037
4171
|
if (options.output) {
|
|
2038
|
-
(0,
|
|
4172
|
+
(0, import_node_fs9.writeFileSync)((0, import_node_path8.resolve)(options.output), json, "utf-8");
|
|
2039
4173
|
} else {
|
|
2040
4174
|
console.log(json);
|
|
2041
4175
|
}
|
|
2042
4176
|
} else {
|
|
2043
|
-
|
|
2044
|
-
try {
|
|
2045
|
-
canvasMod = await import("canvas");
|
|
2046
|
-
} catch {
|
|
2047
|
-
console.error("PNG output requires the 'canvas' package. Install it: npm i canvas");
|
|
2048
|
-
process.exit(1);
|
|
2049
|
-
return;
|
|
2050
|
-
}
|
|
4177
|
+
const { createCanvas: createCanvas2 } = await import("@napi-rs/canvas");
|
|
2051
4178
|
const { renderFrame: renderFrame2 } = await Promise.resolve().then(() => (init_dist3(), dist_exports));
|
|
2052
|
-
const
|
|
2053
|
-
const cvs = createCanvas(doc.canvas.width, doc.canvas.height);
|
|
4179
|
+
const cvs = createCanvas2(doc.canvas.width, doc.canvas.height);
|
|
2054
4180
|
const ctx = cvs.getContext("2d");
|
|
2055
4181
|
renderFrame2(ctx, resolved, doc);
|
|
2056
4182
|
const buffer = cvs.toBuffer("image/png");
|
|
2057
4183
|
if (options.output) {
|
|
2058
|
-
(0,
|
|
4184
|
+
(0, import_node_fs9.writeFileSync)((0, import_node_path8.resolve)(options.output), buffer);
|
|
2059
4185
|
} else {
|
|
2060
4186
|
process.stdout.write(buffer);
|
|
2061
4187
|
}
|
|
@@ -2071,18 +4197,18 @@ function stillCommand(program2) {
|
|
|
2071
4197
|
}
|
|
2072
4198
|
|
|
2073
4199
|
// src/commands/render.ts
|
|
2074
|
-
var
|
|
2075
|
-
var
|
|
4200
|
+
var import_node_fs10 = require("fs");
|
|
4201
|
+
var import_node_path9 = require("path");
|
|
2076
4202
|
|
|
2077
4203
|
// src/commands/render-pipeline.ts
|
|
2078
|
-
var
|
|
4204
|
+
var import_node_child_process3 = require("child_process");
|
|
2079
4205
|
init_dist2();
|
|
2080
4206
|
init_dist3();
|
|
2081
4207
|
async function checkFfmpeg() {
|
|
2082
|
-
return new Promise((
|
|
2083
|
-
const proc = (0,
|
|
2084
|
-
proc.on("error", () =>
|
|
2085
|
-
proc.on("close", (code) =>
|
|
4208
|
+
return new Promise((resolve16) => {
|
|
4209
|
+
const proc = (0, import_node_child_process3.spawn)("ffmpeg", ["-version"], { stdio: "pipe" });
|
|
4210
|
+
proc.on("error", () => resolve16(false));
|
|
4211
|
+
proc.on("close", (code) => resolve16(code === 0));
|
|
2086
4212
|
});
|
|
2087
4213
|
}
|
|
2088
4214
|
function buildFfmpegArgs(width, height, fps, format, output) {
|
|
@@ -2124,7 +4250,7 @@ function buildFfmpegArgs(width, height, fps, format, output) {
|
|
|
2124
4250
|
output
|
|
2125
4251
|
];
|
|
2126
4252
|
}
|
|
2127
|
-
async function
|
|
4253
|
+
async function preloadImages2(doc, loadImage2) {
|
|
2128
4254
|
const sources = /* @__PURE__ */ new Set();
|
|
2129
4255
|
for (const layer of doc.layers) {
|
|
2130
4256
|
if (layer.visual.type === "image") {
|
|
@@ -2143,7 +4269,7 @@ async function preloadImages(doc, loadImage) {
|
|
|
2143
4269
|
await Promise.all(
|
|
2144
4270
|
[...sources].map(async (src) => {
|
|
2145
4271
|
try {
|
|
2146
|
-
preloaded.set(src, await
|
|
4272
|
+
preloaded.set(src, await loadImage2(src));
|
|
2147
4273
|
} catch {
|
|
2148
4274
|
}
|
|
2149
4275
|
})
|
|
@@ -2162,20 +4288,20 @@ async function preloadImages(doc, loadImage) {
|
|
|
2162
4288
|
for (const src of preloaded.keys()) {
|
|
2163
4289
|
imageCache.load(src);
|
|
2164
4290
|
}
|
|
2165
|
-
await new Promise((
|
|
4291
|
+
await new Promise((resolve16) => process.nextTick(resolve16));
|
|
2166
4292
|
return imageCache;
|
|
2167
4293
|
}
|
|
2168
4294
|
async function renderDocument(doc, opts) {
|
|
2169
4295
|
const canvasModuleName = "canvas";
|
|
2170
|
-
let
|
|
2171
|
-
let
|
|
4296
|
+
let createCanvas2;
|
|
4297
|
+
let loadImage2;
|
|
2172
4298
|
try {
|
|
2173
4299
|
const canvasModule = await import(
|
|
2174
4300
|
/* webpackIgnore: true */
|
|
2175
4301
|
canvasModuleName
|
|
2176
4302
|
);
|
|
2177
|
-
|
|
2178
|
-
|
|
4303
|
+
createCanvas2 = canvasModule.createCanvas;
|
|
4304
|
+
loadImage2 = canvasModule.loadImage;
|
|
2179
4305
|
} catch {
|
|
2180
4306
|
throw new Error(
|
|
2181
4307
|
"The 'canvas' package is not installed.\nInstall it with:\n npm install canvas\nPrerequisites vary by OS:\n macOS: brew install pkg-config cairo pango libpng jpeg giflib librsvg pixman\n Ubuntu: sudo apt install build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev\nSee: https://github.com/Automattic/node-canvas#compiling"
|
|
@@ -2204,11 +4330,11 @@ async function renderDocument(doc, opts) {
|
|
|
2204
4330
|
if (totalFrames === 0) {
|
|
2205
4331
|
throw new Error("Nothing to render \u2014 all states have duration 0");
|
|
2206
4332
|
}
|
|
2207
|
-
const imageCache = await
|
|
2208
|
-
const canvas =
|
|
4333
|
+
const imageCache = await preloadImages2(doc, loadImage2);
|
|
4334
|
+
const canvas = createCanvas2(width, height);
|
|
2209
4335
|
const ctx = canvas.getContext("2d");
|
|
2210
4336
|
const ffmpegArgs = buildFfmpegArgs(width, height, fps, format, output);
|
|
2211
|
-
const ffmpeg = (0,
|
|
4337
|
+
const ffmpeg = (0, import_node_child_process3.spawn)("ffmpeg", ffmpegArgs, {
|
|
2212
4338
|
stdio: ["pipe", "pipe", "pipe"]
|
|
2213
4339
|
});
|
|
2214
4340
|
let stderrOutput = "";
|
|
@@ -2226,7 +4352,7 @@ async function renderDocument(doc, opts) {
|
|
|
2226
4352
|
const canWrite = ffmpeg.stdin.write(raw);
|
|
2227
4353
|
if (!canWrite) {
|
|
2228
4354
|
await new Promise(
|
|
2229
|
-
(
|
|
4355
|
+
(resolve16) => ffmpeg.stdin.once("drain", resolve16)
|
|
2230
4356
|
);
|
|
2231
4357
|
}
|
|
2232
4358
|
frameIndex++;
|
|
@@ -2239,8 +4365,8 @@ async function renderDocument(doc, opts) {
|
|
|
2239
4365
|
}
|
|
2240
4366
|
}
|
|
2241
4367
|
ffmpeg.stdin.end();
|
|
2242
|
-
const exitCode = await new Promise((
|
|
2243
|
-
ffmpeg.on("close",
|
|
4368
|
+
const exitCode = await new Promise((resolve16) => {
|
|
4369
|
+
ffmpeg.on("close", resolve16);
|
|
2244
4370
|
});
|
|
2245
4371
|
if (exitCode !== 0) {
|
|
2246
4372
|
throw new Error(
|
|
@@ -2259,10 +4385,10 @@ ${stderrOutput.slice(-500)}`
|
|
|
2259
4385
|
|
|
2260
4386
|
// src/commands/render.ts
|
|
2261
4387
|
function readAndParse3(file) {
|
|
2262
|
-
const absPath = (0,
|
|
4388
|
+
const absPath = (0, import_node_path9.resolve)(file);
|
|
2263
4389
|
let content;
|
|
2264
4390
|
try {
|
|
2265
|
-
content = (0,
|
|
4391
|
+
content = (0, import_node_fs10.readFileSync)(absPath, "utf-8");
|
|
2266
4392
|
} catch {
|
|
2267
4393
|
console.error(`Cannot read file: ${absPath}`);
|
|
2268
4394
|
return process.exit(1);
|
|
@@ -2279,7 +4405,7 @@ function readAndParse3(file) {
|
|
|
2279
4405
|
}
|
|
2280
4406
|
function inferFormat(output) {
|
|
2281
4407
|
if (!output) return "mp4";
|
|
2282
|
-
const ext = (0,
|
|
4408
|
+
const ext = (0, import_node_path9.extname)(output).toLowerCase();
|
|
2283
4409
|
if (ext === ".gif") return "gif";
|
|
2284
4410
|
return "mp4";
|
|
2285
4411
|
}
|
|
@@ -2313,12 +4439,12 @@ function renderCommand(program2) {
|
|
|
2313
4439
|
} else {
|
|
2314
4440
|
format = inferFormat(options.output);
|
|
2315
4441
|
}
|
|
2316
|
-
const inputName = (0,
|
|
4442
|
+
const inputName = (0, import_node_path9.basename)(file, (0, import_node_path9.extname)(file));
|
|
2317
4443
|
const output = options.output ?? `${inputName}.${format}`;
|
|
2318
4444
|
const startTime = Date.now();
|
|
2319
4445
|
try {
|
|
2320
4446
|
const result = await renderDocument(doc, {
|
|
2321
|
-
output: (0,
|
|
4447
|
+
output: (0, import_node_path9.resolve)(output),
|
|
2322
4448
|
format,
|
|
2323
4449
|
states: options.state,
|
|
2324
4450
|
onProgress: ({ frame, totalFrames, state, percent }) => {
|
|
@@ -2344,8 +4470,8 @@ function renderCommand(program2) {
|
|
|
2344
4470
|
}
|
|
2345
4471
|
|
|
2346
4472
|
// src/commands/export-svg.ts
|
|
2347
|
-
var
|
|
2348
|
-
var
|
|
4473
|
+
var import_node_fs11 = require("fs");
|
|
4474
|
+
var import_node_path10 = require("path");
|
|
2349
4475
|
|
|
2350
4476
|
// ../svg/dist/index.js
|
|
2351
4477
|
init_dist2();
|
|
@@ -2810,10 +4936,10 @@ function renderRefSVG(eff, visual, _parentDoc, opts, _depth, _visitedRefs) {
|
|
|
2810
4936
|
|
|
2811
4937
|
// src/commands/export-svg.ts
|
|
2812
4938
|
function readAndParse4(file) {
|
|
2813
|
-
const absPath = (0,
|
|
4939
|
+
const absPath = (0, import_node_path10.resolve)(file);
|
|
2814
4940
|
let content;
|
|
2815
4941
|
try {
|
|
2816
|
-
content = (0,
|
|
4942
|
+
content = (0, import_node_fs11.readFileSync)(absPath, "utf-8");
|
|
2817
4943
|
} catch {
|
|
2818
4944
|
console.error(`Cannot read file: ${absPath}`);
|
|
2819
4945
|
return process.exit(1);
|
|
@@ -2855,7 +4981,7 @@ function exportSvgCommand(program2) {
|
|
|
2855
4981
|
xmlDeclaration: options.xmlDeclaration
|
|
2856
4982
|
});
|
|
2857
4983
|
if (options.output) {
|
|
2858
|
-
(0,
|
|
4984
|
+
(0, import_node_fs11.writeFileSync)((0, import_node_path10.resolve)(options.output), svg, "utf-8");
|
|
2859
4985
|
} else {
|
|
2860
4986
|
console.log(svg);
|
|
2861
4987
|
}
|
|
@@ -2868,8 +4994,8 @@ function exportSvgCommand(program2) {
|
|
|
2868
4994
|
}
|
|
2869
4995
|
|
|
2870
4996
|
// src/commands/export-lottie.ts
|
|
2871
|
-
var
|
|
2872
|
-
var
|
|
4997
|
+
var import_node_fs12 = require("fs");
|
|
4998
|
+
var import_node_path11 = require("path");
|
|
2873
4999
|
|
|
2874
5000
|
// ../lottie/dist/index.js
|
|
2875
5001
|
function colorToLottie(color) {
|
|
@@ -3438,10 +5564,10 @@ function exportToLottie(doc, opts) {
|
|
|
3438
5564
|
|
|
3439
5565
|
// src/commands/export-lottie.ts
|
|
3440
5566
|
function readAndParse5(file) {
|
|
3441
|
-
const absPath = (0,
|
|
5567
|
+
const absPath = (0, import_node_path11.resolve)(file);
|
|
3442
5568
|
let content;
|
|
3443
5569
|
try {
|
|
3444
|
-
content = (0,
|
|
5570
|
+
content = (0, import_node_fs12.readFileSync)(absPath, "utf-8");
|
|
3445
5571
|
} catch {
|
|
3446
5572
|
console.error(`Cannot read file: ${absPath}`);
|
|
3447
5573
|
return process.exit(1);
|
|
@@ -3469,7 +5595,7 @@ function exportLottieCommand(program2) {
|
|
|
3469
5595
|
}
|
|
3470
5596
|
const output = JSON.stringify(json, null, 2);
|
|
3471
5597
|
if (options.output) {
|
|
3472
|
-
(0,
|
|
5598
|
+
(0, import_node_fs12.writeFileSync)((0, import_node_path11.resolve)(options.output), output, "utf-8");
|
|
3473
5599
|
} else {
|
|
3474
5600
|
console.log(output);
|
|
3475
5601
|
}
|
|
@@ -3481,9 +5607,73 @@ function exportLottieCommand(program2) {
|
|
|
3481
5607
|
);
|
|
3482
5608
|
}
|
|
3483
5609
|
|
|
5610
|
+
// src/commands/export-image.ts
|
|
5611
|
+
var import_node_fs13 = require("fs");
|
|
5612
|
+
var import_node_path12 = require("path");
|
|
5613
|
+
function readAndParse6(file) {
|
|
5614
|
+
const absPath = (0, import_node_path12.resolve)(file);
|
|
5615
|
+
let content;
|
|
5616
|
+
try {
|
|
5617
|
+
content = (0, import_node_fs13.readFileSync)(absPath, "utf-8");
|
|
5618
|
+
} catch {
|
|
5619
|
+
console.error(`Cannot read file: ${absPath}`);
|
|
5620
|
+
return process.exit(1);
|
|
5621
|
+
}
|
|
5622
|
+
const result = parseAtelier(content);
|
|
5623
|
+
if (!result.success) {
|
|
5624
|
+
console.error("Parse errors:");
|
|
5625
|
+
for (const error of result.errors) {
|
|
5626
|
+
console.error(` - ${error.path}: ${error.message}`);
|
|
5627
|
+
}
|
|
5628
|
+
return process.exit(1);
|
|
5629
|
+
}
|
|
5630
|
+
return result.data;
|
|
5631
|
+
}
|
|
5632
|
+
function parseDim2(raw, name) {
|
|
5633
|
+
if (raw === void 0) return void 0;
|
|
5634
|
+
const n = parseInt(raw, 10);
|
|
5635
|
+
if (isNaN(n) || n <= 0) {
|
|
5636
|
+
console.error(`Invalid --${name}: ${raw}`);
|
|
5637
|
+
process.exit(1);
|
|
5638
|
+
}
|
|
5639
|
+
return n;
|
|
5640
|
+
}
|
|
5641
|
+
function exportImageCommand(program2) {
|
|
5642
|
+
program2.command("export-image <file>").description(
|
|
5643
|
+
"Export a single frame as a PNG image. Aspect-preserving when only one of --width/--height is set; if both are set the renderer uses both verbatim (may squash)."
|
|
5644
|
+
).requiredOption("-o, --out <path>", "Output PNG file path").option("-s, --state <name>", "State name (defaults to first state)").option("-f, --frame <number>", "Frame number (defaults to 0)", "0").option("--width <number>", "Override output width (px)").option("--height <number>", "Override output height (px)").action(async (file, options) => {
|
|
5645
|
+
const doc = readAndParse6(file);
|
|
5646
|
+
const frameNumber = parseInt(options.frame, 10);
|
|
5647
|
+
if (isNaN(frameNumber) || frameNumber < 0) {
|
|
5648
|
+
console.error(`Invalid frame number: ${options.frame}`);
|
|
5649
|
+
process.exit(1);
|
|
5650
|
+
return;
|
|
5651
|
+
}
|
|
5652
|
+
const width = parseDim2(options.width, "width");
|
|
5653
|
+
const height = parseDim2(options.height, "height");
|
|
5654
|
+
try {
|
|
5655
|
+
const buffer = await renderDocumentToPng(doc, {
|
|
5656
|
+
state: options.state,
|
|
5657
|
+
frame: frameNumber,
|
|
5658
|
+
width,
|
|
5659
|
+
height
|
|
5660
|
+
});
|
|
5661
|
+
(0, import_node_fs13.writeFileSync)((0, import_node_path12.resolve)(options.out), buffer);
|
|
5662
|
+
} catch (err) {
|
|
5663
|
+
if (err instanceof CanvasUnavailableError) {
|
|
5664
|
+
console.error(err.message);
|
|
5665
|
+
process.exit(1);
|
|
5666
|
+
return;
|
|
5667
|
+
}
|
|
5668
|
+
console.error(err.message);
|
|
5669
|
+
process.exit(1);
|
|
5670
|
+
}
|
|
5671
|
+
});
|
|
5672
|
+
}
|
|
5673
|
+
|
|
3484
5674
|
// src/commands/assets.ts
|
|
3485
|
-
var
|
|
3486
|
-
var
|
|
5675
|
+
var import_node_fs14 = require("fs");
|
|
5676
|
+
var import_node_path13 = require("path");
|
|
3487
5677
|
function getAssets(doc) {
|
|
3488
5678
|
const assets = doc.assets ?? {};
|
|
3489
5679
|
return Object.entries(assets).map(([assetId, asset]) => {
|
|
@@ -3514,11 +5704,11 @@ function formatAssets(assets) {
|
|
|
3514
5704
|
}
|
|
3515
5705
|
return lines.join("\n");
|
|
3516
5706
|
}
|
|
3517
|
-
function
|
|
3518
|
-
const absPath = (0,
|
|
5707
|
+
function readAndParse7(file) {
|
|
5708
|
+
const absPath = (0, import_node_path13.resolve)(file);
|
|
3519
5709
|
let content;
|
|
3520
5710
|
try {
|
|
3521
|
-
content = (0,
|
|
5711
|
+
content = (0, import_node_fs14.readFileSync)(absPath, "utf-8");
|
|
3522
5712
|
} catch {
|
|
3523
5713
|
console.error(`Cannot read file: ${absPath}`);
|
|
3524
5714
|
return process.exit(1);
|
|
@@ -3535,15 +5725,15 @@ function readAndParse6(file) {
|
|
|
3535
5725
|
}
|
|
3536
5726
|
function assetsCommand(program2) {
|
|
3537
5727
|
program2.command("assets <file>").description("List all assets in an .atelier file with usage info").action((file) => {
|
|
3538
|
-
const doc =
|
|
5728
|
+
const doc = readAndParse7(file);
|
|
3539
5729
|
const assets = getAssets(doc);
|
|
3540
5730
|
console.log(formatAssets(assets));
|
|
3541
5731
|
});
|
|
3542
5732
|
}
|
|
3543
5733
|
|
|
3544
5734
|
// src/commands/variables.ts
|
|
3545
|
-
var
|
|
3546
|
-
var
|
|
5735
|
+
var import_node_fs15 = require("fs");
|
|
5736
|
+
var import_node_path14 = require("path");
|
|
3547
5737
|
init_dist2();
|
|
3548
5738
|
function getVariables(doc) {
|
|
3549
5739
|
const variables = doc.variables ?? {};
|
|
@@ -3578,11 +5768,11 @@ function formatVariables(info) {
|
|
|
3578
5768
|
}
|
|
3579
5769
|
return lines.join("\n");
|
|
3580
5770
|
}
|
|
3581
|
-
function
|
|
3582
|
-
const absPath = (0,
|
|
5771
|
+
function readAndParse8(file) {
|
|
5772
|
+
const absPath = (0, import_node_path14.resolve)(file);
|
|
3583
5773
|
let content;
|
|
3584
5774
|
try {
|
|
3585
|
-
content = (0,
|
|
5775
|
+
content = (0, import_node_fs15.readFileSync)(absPath, "utf-8");
|
|
3586
5776
|
} catch {
|
|
3587
5777
|
console.error(`Cannot read file: ${absPath}`);
|
|
3588
5778
|
return process.exit(1);
|
|
@@ -3599,48 +5789,160 @@ function readAndParse7(file) {
|
|
|
3599
5789
|
}
|
|
3600
5790
|
function variablesCommand(program2) {
|
|
3601
5791
|
program2.command("variables <file>").description("List all variables in an .atelier file with usage info").action((file) => {
|
|
3602
|
-
const doc =
|
|
5792
|
+
const doc = readAndParse8(file);
|
|
3603
5793
|
const info = getVariables(doc);
|
|
3604
5794
|
console.log(formatVariables(info));
|
|
3605
5795
|
});
|
|
3606
5796
|
}
|
|
3607
5797
|
|
|
3608
5798
|
// src/commands/studio.ts
|
|
3609
|
-
var
|
|
3610
|
-
var
|
|
3611
|
-
var
|
|
5799
|
+
var import_node_path15 = require("path");
|
|
5800
|
+
var import_node_fs16 = require("fs");
|
|
5801
|
+
var import_node_os2 = require("os");
|
|
3612
5802
|
var import_node_crypto = require("crypto");
|
|
3613
|
-
var
|
|
5803
|
+
var import_node_child_process4 = require("child_process");
|
|
5804
|
+
var import_atelier_mcp = require("@a-company/atelier-mcp");
|
|
5805
|
+
var import_ws = require("ws");
|
|
3614
5806
|
var import_meta = {};
|
|
3615
5807
|
function findAtelierFiles(dir, base = dir) {
|
|
3616
5808
|
const results = [];
|
|
3617
5809
|
let entries;
|
|
3618
5810
|
try {
|
|
3619
|
-
entries = (0,
|
|
5811
|
+
entries = (0, import_node_fs16.readdirSync)(dir);
|
|
3620
5812
|
} catch {
|
|
3621
5813
|
return results;
|
|
3622
5814
|
}
|
|
3623
5815
|
for (const entry of entries) {
|
|
3624
5816
|
if (entry === "node_modules" || entry === "dist" || entry === ".git") continue;
|
|
3625
|
-
const full = (0,
|
|
5817
|
+
const full = (0, import_node_path15.join)(dir, entry);
|
|
3626
5818
|
let stat;
|
|
3627
5819
|
try {
|
|
3628
|
-
stat = (0,
|
|
5820
|
+
stat = (0, import_node_fs16.statSync)(full);
|
|
3629
5821
|
} catch {
|
|
3630
5822
|
continue;
|
|
3631
5823
|
}
|
|
3632
5824
|
if (stat.isDirectory()) {
|
|
3633
5825
|
results.push(...findAtelierFiles(full, base));
|
|
3634
5826
|
} else if (entry.endsWith(".atelier")) {
|
|
3635
|
-
results.push((0,
|
|
5827
|
+
results.push((0, import_node_path15.relative)(base, full));
|
|
3636
5828
|
}
|
|
3637
5829
|
}
|
|
3638
5830
|
return results.sort();
|
|
3639
5831
|
}
|
|
3640
5832
|
function isSafePath(filePath) {
|
|
3641
|
-
if (!filePath || filePath.
|
|
3642
|
-
const
|
|
3643
|
-
|
|
5833
|
+
if (!filePath || filePath.startsWith("/")) return false;
|
|
5834
|
+
const cwd = process.cwd();
|
|
5835
|
+
const resolved = (0, import_node_path15.resolve)(cwd, filePath);
|
|
5836
|
+
return resolved === cwd || resolved.startsWith(cwd + import_node_path15.sep);
|
|
5837
|
+
}
|
|
5838
|
+
function writeFileEnsuringDir(absPath, body) {
|
|
5839
|
+
(0, import_node_fs16.mkdirSync)((0, import_node_path15.dirname)(absPath), { recursive: true });
|
|
5840
|
+
(0, import_node_fs16.writeFileSync)(absPath, body, "utf-8");
|
|
5841
|
+
}
|
|
5842
|
+
function isAllowedOrigin(origin, port) {
|
|
5843
|
+
if (!origin) return false;
|
|
5844
|
+
return origin === `http://localhost:${port}` || origin === `http://127.0.0.1:${port}`;
|
|
5845
|
+
}
|
|
5846
|
+
function isAllowedMcpOrigin(origin, port) {
|
|
5847
|
+
return origin === void 0 || isAllowedOrigin(origin, port);
|
|
5848
|
+
}
|
|
5849
|
+
function shouldBroadcastMutation(source) {
|
|
5850
|
+
return source === "llm";
|
|
5851
|
+
}
|
|
5852
|
+
function attachBridgeClient(ws, state, clients, loadDocFromDisk, persistHumanPatch) {
|
|
5853
|
+
clients.add(ws);
|
|
5854
|
+
const clientId = (0, import_node_crypto.randomBytes)(6).toString("hex");
|
|
5855
|
+
const send = (env) => {
|
|
5856
|
+
if (ws.readyState !== ws.OPEN) return;
|
|
5857
|
+
ws.send(JSON.stringify(env));
|
|
5858
|
+
};
|
|
5859
|
+
send({ type: "hello", clientId, protocolVersion: import_atelier_mcp.BRIDGE_PROTOCOL_VERSION });
|
|
5860
|
+
if (state.currentDocId) {
|
|
5861
|
+
const existing = state.store.get(state.currentDocId);
|
|
5862
|
+
if (existing) {
|
|
5863
|
+
send({ type: "doc:loaded", documentId: state.currentDocId, doc: existing });
|
|
5864
|
+
} else {
|
|
5865
|
+
const raw = loadDocFromDisk(state.currentDocId);
|
|
5866
|
+
if (raw) {
|
|
5867
|
+
const parsed = parseAtelier(raw);
|
|
5868
|
+
if (parsed.success) {
|
|
5869
|
+
state.store.set(state.currentDocId, parsed.data, "system");
|
|
5870
|
+
send({ type: "doc:loaded", documentId: state.currentDocId, doc: parsed.data });
|
|
5871
|
+
}
|
|
5872
|
+
}
|
|
5873
|
+
}
|
|
5874
|
+
}
|
|
5875
|
+
const onMessage = (data) => {
|
|
5876
|
+
let text;
|
|
5877
|
+
if (typeof data === "string") text = data;
|
|
5878
|
+
else if (data instanceof Buffer) text = data.toString("utf-8");
|
|
5879
|
+
else if (Array.isArray(data)) text = Buffer.concat(data).toString("utf-8");
|
|
5880
|
+
else text = String(data);
|
|
5881
|
+
let env;
|
|
5882
|
+
try {
|
|
5883
|
+
env = JSON.parse(text);
|
|
5884
|
+
} catch {
|
|
5885
|
+
send({ type: "error", code: "parse_error", message: "invalid JSON" });
|
|
5886
|
+
return;
|
|
5887
|
+
}
|
|
5888
|
+
if (!(0, import_atelier_mcp.isBridgeEnvelope)(env)) {
|
|
5889
|
+
send({ type: "error", code: "invalid_envelope", message: "unknown envelope shape" });
|
|
5890
|
+
return;
|
|
5891
|
+
}
|
|
5892
|
+
if (env.type === "doc:patch") {
|
|
5893
|
+
try {
|
|
5894
|
+
state.store.set(env.documentId, env.doc, "human");
|
|
5895
|
+
state.currentDocId = env.documentId;
|
|
5896
|
+
persistHumanPatch(env.documentId, env.doc);
|
|
5897
|
+
} catch (err) {
|
|
5898
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
5899
|
+
send({ type: "error", code: "persist_failed", message: msg, opId: env.opId });
|
|
5900
|
+
}
|
|
5901
|
+
return;
|
|
5902
|
+
}
|
|
5903
|
+
if (env.type === "doc:load") {
|
|
5904
|
+
const existing = state.store.get(env.documentId);
|
|
5905
|
+
if (existing) {
|
|
5906
|
+
send({ type: "doc:loaded", documentId: env.documentId, doc: existing });
|
|
5907
|
+
return;
|
|
5908
|
+
}
|
|
5909
|
+
const raw = loadDocFromDisk(env.documentId);
|
|
5910
|
+
if (raw) {
|
|
5911
|
+
const parsed = parseAtelier(raw);
|
|
5912
|
+
if (parsed.success) {
|
|
5913
|
+
state.store.set(env.documentId, parsed.data, "system");
|
|
5914
|
+
send({ type: "doc:loaded", documentId: env.documentId, doc: parsed.data });
|
|
5915
|
+
return;
|
|
5916
|
+
}
|
|
5917
|
+
}
|
|
5918
|
+
send({ type: "error", code: "not_found", message: `document ${env.documentId} not found` });
|
|
5919
|
+
return;
|
|
5920
|
+
}
|
|
5921
|
+
};
|
|
5922
|
+
const onClose = () => {
|
|
5923
|
+
clients.delete(ws);
|
|
5924
|
+
};
|
|
5925
|
+
ws.on("message", onMessage);
|
|
5926
|
+
ws.on("close", onClose);
|
|
5927
|
+
ws.on("error", onClose);
|
|
5928
|
+
return () => {
|
|
5929
|
+
clients.delete(ws);
|
|
5930
|
+
try {
|
|
5931
|
+
ws.close();
|
|
5932
|
+
} catch {
|
|
5933
|
+
}
|
|
5934
|
+
};
|
|
5935
|
+
}
|
|
5936
|
+
function broadcastToBridge(clients, envelope) {
|
|
5937
|
+
const payload = JSON.stringify(envelope);
|
|
5938
|
+
for (const ws of clients) {
|
|
5939
|
+
if (ws.readyState === ws.OPEN) {
|
|
5940
|
+
try {
|
|
5941
|
+
ws.send(payload);
|
|
5942
|
+
} catch {
|
|
5943
|
+
}
|
|
5944
|
+
}
|
|
5945
|
+
}
|
|
3644
5946
|
}
|
|
3645
5947
|
function getInlineHTML() {
|
|
3646
5948
|
return `<!DOCTYPE html>
|
|
@@ -3659,431 +5961,59 @@ function getInlineHTML() {
|
|
|
3659
5961
|
</body>
|
|
3660
5962
|
</html>`;
|
|
3661
5963
|
}
|
|
3662
|
-
function getInlineApp(initialFile) {
|
|
5964
|
+
function getInlineApp(initialFile, cliPackageDir) {
|
|
3663
5965
|
const initialFileStr = initialFile ? JSON.stringify(initialFile) : "null";
|
|
3664
|
-
|
|
3665
|
-
import
|
|
3666
|
-
|
|
3667
|
-
|
|
3668
|
-
// \u2500\u2500 Types \u2500\u2500
|
|
3669
|
-
interface FileEntry {
|
|
3670
|
-
path: string;
|
|
3671
|
-
name: string;
|
|
3672
|
-
folder: string;
|
|
3673
|
-
}
|
|
3674
|
-
|
|
3675
|
-
// \u2500\u2500 State \u2500\u2500
|
|
3676
|
-
let studio: AtelierStudio | null = null;
|
|
3677
|
-
let currentFile: string | null = null;
|
|
3678
|
-
let files: FileEntry[] = [];
|
|
3679
|
-
let saveTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
3680
|
-
|
|
3681
|
-
// \u2500\u2500 API helpers \u2500\u2500
|
|
3682
|
-
async function fetchFiles(): Promise<FileEntry[]> {
|
|
3683
|
-
const res = await fetch("/api/files");
|
|
3684
|
-
return res.json();
|
|
3685
|
-
}
|
|
3686
|
-
|
|
3687
|
-
async function fetchFileContent(path: string): Promise<string> {
|
|
3688
|
-
const res = await fetch("/api/file?path=" + encodeURIComponent(path));
|
|
3689
|
-
return res.text();
|
|
3690
|
-
}
|
|
3691
|
-
|
|
3692
|
-
async function saveFileContent(path: string, content: string): Promise<void> {
|
|
3693
|
-
await fetch("/api/file?path=" + encodeURIComponent(path), {
|
|
3694
|
-
method: "POST",
|
|
3695
|
-
headers: { "Content-Type": "text/plain" },
|
|
3696
|
-
body: content,
|
|
3697
|
-
});
|
|
3698
|
-
}
|
|
3699
|
-
|
|
3700
|
-
async function saveExportBlob(path: string, blob: Blob): Promise<void> {
|
|
3701
|
-
const buf = await blob.arrayBuffer();
|
|
3702
|
-
await fetch("/api/export?path=" + encodeURIComponent(path), {
|
|
3703
|
-
method: "POST",
|
|
3704
|
-
headers: { "Content-Type": "application/octet-stream" },
|
|
3705
|
-
body: buf,
|
|
3706
|
-
});
|
|
5966
|
+
const appModulePath = (0, import_node_path15.join)(cliPackageDir, "src", "web", "inline-app.ts").split(import_node_path15.sep).join("/");
|
|
5967
|
+
return `import { bootStudioApp } from ${JSON.stringify(appModulePath)};
|
|
5968
|
+
bootStudioApp({ initialFile: ${initialFileStr} });
|
|
5969
|
+
`;
|
|
3707
5970
|
}
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
|
|
3711
|
-
|
|
3712
|
-
|
|
3713
|
-
|
|
3714
|
-
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
overlay.appendChild(card);
|
|
3718
|
-
document.body.appendChild(overlay);
|
|
3719
|
-
|
|
3720
|
-
const title = document.createElement("div");
|
|
3721
|
-
title.style.cssText = "font-size:18px;margin-bottom:16px;font-weight:600";
|
|
3722
|
-
title.textContent = "Exporting All Files\u2026";
|
|
3723
|
-
card.appendChild(title);
|
|
3724
|
-
|
|
3725
|
-
const fileLabel = document.createElement("div");
|
|
3726
|
-
fileLabel.style.cssText = "font-size:13px;color:#A89F95;margin-bottom:8px;font-family:'SF Mono','Fira Code',monospace";
|
|
3727
|
-
card.appendChild(fileLabel);
|
|
3728
|
-
|
|
3729
|
-
const progress = document.createElement("progress");
|
|
3730
|
-
progress.style.cssText = "width:100%;height:6px;appearance:none;-webkit-appearance:none";
|
|
3731
|
-
progress.max = files.length;
|
|
3732
|
-
progress.value = 0;
|
|
3733
|
-
card.appendChild(progress);
|
|
3734
|
-
|
|
3735
|
-
const statusText = document.createElement("div");
|
|
3736
|
-
statusText.style.cssText = "font-size:12px;color:#A89F95;margin-top:8px";
|
|
3737
|
-
card.appendChild(statusText);
|
|
3738
|
-
|
|
3739
|
-
let exported = 0;
|
|
3740
|
-
let errors = 0;
|
|
3741
|
-
|
|
3742
|
-
for (const file of files) {
|
|
3743
|
-
fileLabel.textContent = file.path;
|
|
3744
|
-
statusText.textContent = (exported + errors + 1) + " / " + files.length;
|
|
3745
|
-
|
|
3746
|
-
try {
|
|
3747
|
-
const content = await fetchFileContent(file.path);
|
|
3748
|
-
const result = parseAtelier(content);
|
|
3749
|
-
if (!result.success) {
|
|
3750
|
-
errors++;
|
|
3751
|
-
progress.value = exported + errors;
|
|
3752
|
-
continue;
|
|
3753
|
-
}
|
|
3754
|
-
|
|
3755
|
-
const doc = result.data;
|
|
3756
|
-
const w = doc.canvas.width;
|
|
3757
|
-
const h = doc.canvas.height;
|
|
3758
|
-
const canvas = document.createElement("canvas");
|
|
3759
|
-
canvas.width = w;
|
|
3760
|
-
canvas.height = h;
|
|
3761
|
-
const imageCache = new ImageCache();
|
|
3762
|
-
|
|
3763
|
-
const exportResult = await exportDocument(doc, canvas, imageCache, {
|
|
3764
|
-
format,
|
|
3765
|
-
onProgress: ({ percent }) => {
|
|
3766
|
-
statusText.textContent = (exported + errors + 1) + " / " + files.length + " \u2014 " + percent + "%";
|
|
3767
|
-
},
|
|
3768
|
-
});
|
|
3769
|
-
|
|
3770
|
-
// Save alongside the source file: e.g. "dir/my-anim.atelier" \u2192 "dir/my-anim.gif"
|
|
3771
|
-
const outPath = file.path.replace(/\\.atelier$/, "." + format);
|
|
3772
|
-
await saveExportBlob(outPath, exportResult.blob);
|
|
3773
|
-
exported++;
|
|
3774
|
-
} catch (e) {
|
|
3775
|
-
console.error("Export failed:", file.path, e);
|
|
3776
|
-
errors++;
|
|
5971
|
+
function resolveCliPackageDir() {
|
|
5972
|
+
const here = (0, import_node_path15.dirname)(new URL(import_meta.url).pathname);
|
|
5973
|
+
const candidates = [
|
|
5974
|
+
(0, import_node_path15.resolve)(here, ".."),
|
|
5975
|
+
(0, import_node_path15.resolve)(here, "..", "..")
|
|
5976
|
+
];
|
|
5977
|
+
for (const c of candidates) {
|
|
5978
|
+
if ((0, import_node_fs16.existsSync)((0, import_node_path15.join)(c, "package.json"))) {
|
|
5979
|
+
return c;
|
|
3777
5980
|
}
|
|
3778
|
-
progress.value = exported + errors;
|
|
3779
5981
|
}
|
|
3780
|
-
|
|
3781
|
-
// Done
|
|
3782
|
-
title.textContent = "Export Complete";
|
|
3783
|
-
fileLabel.textContent = "";
|
|
3784
|
-
statusText.textContent = exported + " exported" + (errors > 0 ? ", " + errors + " failed" : "");
|
|
3785
|
-
if (errors > 0) console.warn("Export All finished with " + errors + " error(s). Check console for details.");
|
|
3786
|
-
|
|
3787
|
-
const closeBtn = document.createElement("button");
|
|
3788
|
-
closeBtn.style.cssText = "margin-top:16px;padding:6px 20px;background:#3D3D3D;color:#F5F0EB;border:1px solid #4A4A4A;border-radius:4px;cursor:pointer;font-family:inherit;font-size:13px";
|
|
3789
|
-
closeBtn.textContent = "Close";
|
|
3790
|
-
closeBtn.addEventListener("click", () => document.body.removeChild(overlay));
|
|
3791
|
-
card.appendChild(closeBtn);
|
|
5982
|
+
return candidates[0];
|
|
3792
5983
|
}
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
buttonActive: "#555555",
|
|
3806
|
-
accent: "#C75B39",
|
|
3807
|
-
accentHover: "#D4724E",
|
|
3808
|
-
sliderTrack: "#4A4A4A",
|
|
3809
|
-
sliderThumb: "#C75B39",
|
|
3810
|
-
fontFamily: "'Cormorant Garamond', Georgia, serif",
|
|
3811
|
-
fontMono: "'SF Mono', 'Fira Code', monospace",
|
|
3812
|
-
canvasShadow: "0 4px 60px rgba(199, 91, 57, 0.12), 0 0 40px rgba(0,0,0,0.4)",
|
|
3813
|
-
};
|
|
3814
|
-
|
|
3815
|
-
// \u2500\u2500 Styles \u2500\u2500
|
|
3816
|
-
const style = document.createElement("style");
|
|
3817
|
-
style.textContent = \`
|
|
3818
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
3819
|
-
html, body { height: 100%; overflow: hidden; background: #2C2C2C; color: #F5F0EB; }
|
|
3820
|
-
body { font-family: 'Cormorant Garamond', Georgia, serif; }
|
|
3821
|
-
#studio { display: flex; height: 100vh; width: 100vw; }
|
|
3822
|
-
|
|
3823
|
-
.sidebar {
|
|
3824
|
-
width: 260px;
|
|
3825
|
-
min-width: 260px;
|
|
3826
|
-
background: #333333;
|
|
3827
|
-
border-right: 1px solid #4A4A4A;
|
|
3828
|
-
display: flex;
|
|
3829
|
-
flex-direction: column;
|
|
3830
|
-
overflow: hidden;
|
|
3831
|
-
}
|
|
3832
|
-
.sidebar__header {
|
|
3833
|
-
padding: 16px 20px;
|
|
3834
|
-
border-bottom: 1px solid #4A4A4A;
|
|
3835
|
-
font-size: 11px;
|
|
3836
|
-
font-weight: 600;
|
|
3837
|
-
letter-spacing: 2px;
|
|
3838
|
-
text-transform: uppercase;
|
|
3839
|
-
color: #A89F95;
|
|
3840
|
-
display: flex;
|
|
3841
|
-
align-items: center;
|
|
3842
|
-
gap: 8px;
|
|
3843
|
-
}
|
|
3844
|
-
.sidebar__header span {
|
|
3845
|
-
color: #C75B39;
|
|
3846
|
-
font-size: 13px;
|
|
3847
|
-
}
|
|
3848
|
-
.sidebar__list {
|
|
3849
|
-
flex: 1;
|
|
3850
|
-
overflow-y: auto;
|
|
3851
|
-
padding: 8px 0;
|
|
3852
|
-
}
|
|
3853
|
-
.sidebar__list::-webkit-scrollbar { width: 6px; }
|
|
3854
|
-
.sidebar__list::-webkit-scrollbar-track { background: transparent; }
|
|
3855
|
-
.sidebar__list::-webkit-scrollbar-thumb { background: #4A4A4A; border-radius: 3px; }
|
|
3856
|
-
|
|
3857
|
-
.sidebar__folder {
|
|
3858
|
-
padding: 10px 20px 4px;
|
|
3859
|
-
font-size: 10px;
|
|
3860
|
-
font-weight: 600;
|
|
3861
|
-
letter-spacing: 1.5px;
|
|
3862
|
-
text-transform: uppercase;
|
|
3863
|
-
color: #A89F95;
|
|
3864
|
-
}
|
|
3865
|
-
.sidebar__item {
|
|
3866
|
-
padding: 8px 20px 8px 28px;
|
|
3867
|
-
font-size: 13px;
|
|
3868
|
-
cursor: pointer;
|
|
3869
|
-
color: #A89F95;
|
|
3870
|
-
transition: background 0.15s, color 0.15s;
|
|
3871
|
-
white-space: nowrap;
|
|
3872
|
-
overflow: hidden;
|
|
3873
|
-
text-overflow: ellipsis;
|
|
3874
|
-
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
3875
|
-
font-size: 11.5px;
|
|
3876
|
-
}
|
|
3877
|
-
.sidebar__item:hover { background: #363636; color: #F5F0EB; }
|
|
3878
|
-
.sidebar__item--active {
|
|
3879
|
-
background: rgba(199, 91, 57, 0.12) !important;
|
|
3880
|
-
color: #C75B39 !important;
|
|
5984
|
+
function scaffoldWelcomeIfEmpty(cwd, cliPackageDir) {
|
|
5985
|
+
const existing = findAtelierFiles(cwd);
|
|
5986
|
+
if (existing.length > 0) return null;
|
|
5987
|
+
const templatesDir = (0, import_node_path15.join)(cliPackageDir, "templates");
|
|
5988
|
+
const welcomeSrc = (0, import_node_path15.join)(templatesDir, "welcome.atelier");
|
|
5989
|
+
if (!(0, import_node_fs16.existsSync)(welcomeSrc)) return null;
|
|
5990
|
+
const welcomeDest = (0, import_node_path15.join)(cwd, "welcome.atelier");
|
|
5991
|
+
if ((0, import_node_fs16.existsSync)(welcomeDest)) return null;
|
|
5992
|
+
try {
|
|
5993
|
+
(0, import_node_fs16.copyFileSync)(welcomeSrc, welcomeDest);
|
|
5994
|
+
} catch {
|
|
5995
|
+
return null;
|
|
3881
5996
|
}
|
|
3882
|
-
|
|
3883
|
-
.
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
height: 32px;
|
|
3891
|
-
min-height: 32px;
|
|
3892
|
-
display: flex;
|
|
3893
|
-
align-items: center;
|
|
3894
|
-
padding: 0 16px;
|
|
3895
|
-
background: #333333;
|
|
3896
|
-
border-bottom: 1px solid #4A4A4A;
|
|
3897
|
-
font-size: 11px;
|
|
3898
|
-
color: #A89F95;
|
|
3899
|
-
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
3900
|
-
gap: 12px;
|
|
3901
|
-
}
|
|
3902
|
-
.main__status .save-indicator {
|
|
3903
|
-
display: inline-flex;
|
|
3904
|
-
align-items: center;
|
|
3905
|
-
gap: 4px;
|
|
3906
|
-
margin-left: auto;
|
|
3907
|
-
transition: opacity 0.3s;
|
|
3908
|
-
}
|
|
3909
|
-
.main__status .save-indicator--saving { color: #C75B39; }
|
|
3910
|
-
.main__status .save-indicator--saved { color: #6B8E6B; }
|
|
3911
|
-
.main__editor {
|
|
3912
|
-
flex: 1;
|
|
3913
|
-
overflow: hidden;
|
|
3914
|
-
}
|
|
3915
|
-
.main__empty {
|
|
3916
|
-
flex: 1;
|
|
3917
|
-
display: flex;
|
|
3918
|
-
align-items: center;
|
|
3919
|
-
justify-content: center;
|
|
3920
|
-
color: #A89F95;
|
|
3921
|
-
font-size: 18px;
|
|
3922
|
-
}
|
|
3923
|
-
\`;
|
|
3924
|
-
document.head.appendChild(style);
|
|
3925
|
-
|
|
3926
|
-
// \u2500\u2500 Build UI \u2500\u2500
|
|
3927
|
-
const root = document.getElementById("studio")!;
|
|
3928
|
-
const sidebar = document.createElement("div");
|
|
3929
|
-
sidebar.className = "sidebar";
|
|
3930
|
-
|
|
3931
|
-
const sidebarHeader = document.createElement("div");
|
|
3932
|
-
sidebarHeader.className = "sidebar__header";
|
|
3933
|
-
sidebarHeader.innerHTML = '<span>◆</span> ATELIER STUDIO';
|
|
3934
|
-
sidebar.appendChild(sidebarHeader);
|
|
3935
|
-
|
|
3936
|
-
const sidebarList = document.createElement("div");
|
|
3937
|
-
sidebarList.className = "sidebar__list";
|
|
3938
|
-
sidebar.appendChild(sidebarList);
|
|
3939
|
-
|
|
3940
|
-
const sidebarFooter = document.createElement("div");
|
|
3941
|
-
sidebarFooter.style.cssText = "padding:12px 16px;border-top:1px solid #4A4A4A;display:flex;gap:8px;align-items:center";
|
|
3942
|
-
const exportAllSelect = document.createElement("select");
|
|
3943
|
-
exportAllSelect.style.cssText = "flex:1;background:#3D3D3D;color:#F5F0EB;border:1px solid #4A4A4A;border-radius:4px;padding:4px 8px;font-size:11px;font-family:'SF Mono','Fira Code',monospace;cursor:pointer";
|
|
3944
|
-
for (const [val, label] of [["gif","GIF"],["mp4","MP4"],["webm","WebM"]] as const) {
|
|
3945
|
-
const o = document.createElement("option");
|
|
3946
|
-
o.value = val;
|
|
3947
|
-
o.textContent = label;
|
|
3948
|
-
exportAllSelect.appendChild(o);
|
|
3949
|
-
}
|
|
3950
|
-
sidebarFooter.appendChild(exportAllSelect);
|
|
3951
|
-
const exportAllBtn = document.createElement("button");
|
|
3952
|
-
exportAllBtn.style.cssText = "background:#C75B39;color:#F5F0EB;border:none;border-radius:4px;padding:5px 12px;font-size:11px;font-family:inherit;cursor:pointer;white-space:nowrap";
|
|
3953
|
-
exportAllBtn.textContent = "Export All";
|
|
3954
|
-
exportAllBtn.addEventListener("click", () => {
|
|
3955
|
-
exportAll(exportAllSelect.value as "gif" | "mp4" | "webm");
|
|
3956
|
-
});
|
|
3957
|
-
sidebarFooter.appendChild(exportAllBtn);
|
|
3958
|
-
sidebar.appendChild(sidebarFooter);
|
|
3959
|
-
|
|
3960
|
-
const main = document.createElement("div");
|
|
3961
|
-
main.className = "main";
|
|
3962
|
-
|
|
3963
|
-
const statusBar = document.createElement("div");
|
|
3964
|
-
statusBar.className = "main__status";
|
|
3965
|
-
main.appendChild(statusBar);
|
|
3966
|
-
|
|
3967
|
-
const editorContainer = document.createElement("div");
|
|
3968
|
-
editorContainer.className = "main__editor";
|
|
3969
|
-
main.appendChild(editorContainer);
|
|
3970
|
-
|
|
3971
|
-
root.appendChild(sidebar);
|
|
3972
|
-
root.appendChild(main);
|
|
3973
|
-
|
|
3974
|
-
// \u2500\u2500 File list rendering \u2500\u2500
|
|
3975
|
-
function renderFileList(): void {
|
|
3976
|
-
sidebarList.innerHTML = "";
|
|
3977
|
-
let lastFolder = "";
|
|
3978
|
-
|
|
3979
|
-
for (const file of files) {
|
|
3980
|
-
if (file.folder && file.folder !== lastFolder) {
|
|
3981
|
-
lastFolder = file.folder;
|
|
3982
|
-
const folder = document.createElement("div");
|
|
3983
|
-
folder.className = "sidebar__folder";
|
|
3984
|
-
folder.textContent = file.folder;
|
|
3985
|
-
sidebarList.appendChild(folder);
|
|
5997
|
+
const bgSrc = (0, import_node_path15.join)(templatesDir, "welcome-bg.png");
|
|
5998
|
+
if ((0, import_node_fs16.existsSync)(bgSrc)) {
|
|
5999
|
+
const bgDest = (0, import_node_path15.join)(cwd, "welcome-bg.png");
|
|
6000
|
+
if (!(0, import_node_fs16.existsSync)(bgDest)) {
|
|
6001
|
+
try {
|
|
6002
|
+
(0, import_node_fs16.copyFileSync)(bgSrc, bgDest);
|
|
6003
|
+
} catch {
|
|
6004
|
+
}
|
|
3986
6005
|
}
|
|
3987
|
-
|
|
3988
|
-
const item = document.createElement("div");
|
|
3989
|
-
item.className = "sidebar__item" + (file.path === currentFile ? " sidebar__item--active" : "");
|
|
3990
|
-
item.textContent = file.name;
|
|
3991
|
-
item.title = file.path;
|
|
3992
|
-
item.addEventListener("click", () => loadFile(file.path));
|
|
3993
|
-
sidebarList.appendChild(item);
|
|
3994
|
-
}
|
|
3995
|
-
}
|
|
3996
|
-
|
|
3997
|
-
// \u2500\u2500 Load a file into the studio \u2500\u2500
|
|
3998
|
-
async function loadFile(path: string): Promise<void> {
|
|
3999
|
-
currentFile = path;
|
|
4000
|
-
renderFileList();
|
|
4001
|
-
|
|
4002
|
-
const content = await fetchFileContent(path);
|
|
4003
|
-
const result = parseAtelier(content);
|
|
4004
|
-
|
|
4005
|
-
if (!result.success) {
|
|
4006
|
-
editorContainer.innerHTML = "";
|
|
4007
|
-
const err = document.createElement("div");
|
|
4008
|
-
err.className = "main__empty";
|
|
4009
|
-
err.style.flexDirection = "column";
|
|
4010
|
-
err.style.gap = "8px";
|
|
4011
|
-
err.innerHTML = '<div style="color:#C75B39">Parse Error</div><div style="font-size:13px;font-family:monospace">' +
|
|
4012
|
-
result.errors.map(e => e.path + ": " + e.message).join("<br>") + "</div>";
|
|
4013
|
-
editorContainer.appendChild(err);
|
|
4014
|
-
return;
|
|
4015
|
-
}
|
|
4016
|
-
|
|
4017
|
-
statusBar.innerHTML = '<span>' + path + '</span><span class="save-indicator save-indicator--saved">✓ saved</span>';
|
|
4018
|
-
|
|
4019
|
-
if (studio) {
|
|
4020
|
-
studio.destroy();
|
|
4021
|
-
studio = null;
|
|
4022
6006
|
}
|
|
4023
|
-
|
|
4024
|
-
// Set filename for export downloads (strip path and .atelier extension)
|
|
4025
|
-
const baseName = path.split("/").pop()?.replace(/\\.atelier$/, "") || null;
|
|
4026
|
-
|
|
4027
|
-
studio = new AtelierStudio(editorContainer, {
|
|
4028
|
-
mode: "full",
|
|
4029
|
-
initialTab: "yaml",
|
|
4030
|
-
allowSave: true,
|
|
4031
|
-
onDocumentChange: (doc) => {
|
|
4032
|
-
// Auto-save with debounce
|
|
4033
|
-
const indicator = statusBar.querySelector(".save-indicator");
|
|
4034
|
-
if (indicator) {
|
|
4035
|
-
indicator.className = "save-indicator save-indicator--saving";
|
|
4036
|
-
indicator.innerHTML = "● saving...";
|
|
4037
|
-
}
|
|
4038
|
-
|
|
4039
|
-
if (saveTimeout) clearTimeout(saveTimeout);
|
|
4040
|
-
saveTimeout = setTimeout(async () => {
|
|
4041
|
-
if (!currentFile) return;
|
|
4042
|
-
const yaml = serializeAtelier(doc);
|
|
4043
|
-
await saveFileContent(currentFile, yaml);
|
|
4044
|
-
const ind = statusBar.querySelector(".save-indicator");
|
|
4045
|
-
if (ind) {
|
|
4046
|
-
ind.className = "save-indicator save-indicator--saved";
|
|
4047
|
-
ind.innerHTML = "✓ saved";
|
|
4048
|
-
}
|
|
4049
|
-
}, 800);
|
|
4050
|
-
},
|
|
4051
|
-
});
|
|
4052
|
-
studio.setTheme(theme);
|
|
4053
|
-
studio.setFilename(baseName);
|
|
4054
|
-
studio.loadDocument(result.data);
|
|
6007
|
+
return "welcome.atelier";
|
|
4055
6008
|
}
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
editorContainer.innerHTML = "";
|
|
4063
|
-
const empty = document.createElement("div");
|
|
4064
|
-
empty.className = "main__empty";
|
|
4065
|
-
empty.textContent = "No .atelier files found in this directory";
|
|
4066
|
-
editorContainer.appendChild(empty);
|
|
4067
|
-
statusBar.textContent = "No files";
|
|
4068
|
-
renderFileList();
|
|
4069
|
-
return;
|
|
4070
|
-
}
|
|
4071
|
-
|
|
4072
|
-
renderFileList();
|
|
4073
|
-
|
|
4074
|
-
const initialFile = ${initialFileStr};
|
|
4075
|
-
const target = initialFile
|
|
4076
|
-
? files.find(f => f.path === initialFile || f.path.endsWith(initialFile))
|
|
4077
|
-
: files[0];
|
|
4078
|
-
|
|
4079
|
-
if (target) {
|
|
4080
|
-
await loadFile(target.path);
|
|
6009
|
+
function readCliVersion(cliPackageDir) {
|
|
6010
|
+
try {
|
|
6011
|
+
const pkg = JSON.parse((0, import_node_fs16.readFileSync)((0, import_node_path15.join)(cliPackageDir, "package.json"), "utf-8"));
|
|
6012
|
+
return typeof pkg.version === "string" ? pkg.version : "unknown";
|
|
6013
|
+
} catch {
|
|
6014
|
+
return "unknown";
|
|
4081
6015
|
}
|
|
4082
6016
|
}
|
|
4083
|
-
|
|
4084
|
-
boot();
|
|
4085
|
-
`;
|
|
4086
|
-
}
|
|
4087
6017
|
function studioCommand(program2) {
|
|
4088
6018
|
program2.command("studio [file]").description("Launch the browser-based Atelier editor").option("-p, --port <number>", "Port to serve on", "4321").option("--no-open", "Don't auto-open browser").action(
|
|
4089
6019
|
async (file, options) => {
|
|
@@ -4093,21 +6023,27 @@ function studioCommand(program2) {
|
|
|
4093
6023
|
process.exit(1);
|
|
4094
6024
|
}
|
|
4095
6025
|
const cwd = process.cwd();
|
|
4096
|
-
const cliPackageDir = (
|
|
6026
|
+
const cliPackageDir = resolveCliPackageDir();
|
|
6027
|
+
const version = readCliVersion(cliPackageDir);
|
|
6028
|
+
const scaffolded = scaffoldWelcomeIfEmpty(cwd, cliPackageDir);
|
|
6029
|
+
console.log("");
|
|
6030
|
+
console.log(` Atelier Studio \xB7 v${version}`);
|
|
6031
|
+
if (scaffolded) {
|
|
6032
|
+
console.log(` Scaffolded ${scaffolded} \u2014 opening\u2026`);
|
|
6033
|
+
}
|
|
4097
6034
|
const tmpId = (0, import_node_crypto.randomBytes)(4).toString("hex");
|
|
4098
|
-
const tmpDirRaw = (0,
|
|
4099
|
-
(0,
|
|
4100
|
-
const tmpDir = (0,
|
|
4101
|
-
(0,
|
|
4102
|
-
(0,
|
|
4103
|
-
const cliNodeModules = (0,
|
|
4104
|
-
if ((0,
|
|
6035
|
+
const tmpDirRaw = (0, import_node_path15.join)((0, import_node_os2.tmpdir)(), `atelier-studio-${tmpId}`);
|
|
6036
|
+
(0, import_node_fs16.mkdirSync)(tmpDirRaw, { recursive: true });
|
|
6037
|
+
const tmpDir = (0, import_node_fs16.realpathSync)(tmpDirRaw);
|
|
6038
|
+
(0, import_node_fs16.writeFileSync)((0, import_node_path15.join)(tmpDir, "index.html"), getInlineHTML());
|
|
6039
|
+
(0, import_node_fs16.writeFileSync)((0, import_node_path15.join)(tmpDir, "main.ts"), getInlineApp(file ?? null, cliPackageDir));
|
|
6040
|
+
const cliNodeModules = (0, import_node_path15.join)(cliPackageDir, "node_modules");
|
|
6041
|
+
if ((0, import_node_fs16.existsSync)(cliNodeModules)) {
|
|
4105
6042
|
try {
|
|
4106
|
-
(0,
|
|
6043
|
+
(0, import_node_fs16.symlinkSync)(cliNodeModules, (0, import_node_path15.join)(tmpDir, "node_modules"), "dir");
|
|
4107
6044
|
} catch {
|
|
4108
6045
|
}
|
|
4109
6046
|
}
|
|
4110
|
-
console.log(`Starting Atelier Studio...`);
|
|
4111
6047
|
console.log(` Working directory: ${cwd}`);
|
|
4112
6048
|
let vite;
|
|
4113
6049
|
try {
|
|
@@ -4118,9 +6054,42 @@ function studioCommand(program2) {
|
|
|
4118
6054
|
process.exit(1);
|
|
4119
6055
|
return;
|
|
4120
6056
|
}
|
|
6057
|
+
const HOSTNAME = "127.0.0.1";
|
|
6058
|
+
const bridgeState = {
|
|
6059
|
+
store: new import_atelier_mcp.DocumentStore(),
|
|
6060
|
+
currentDocId: null
|
|
6061
|
+
};
|
|
6062
|
+
const bridgeClients = /* @__PURE__ */ new Set();
|
|
6063
|
+
const loadDocFromDisk = (docId) => {
|
|
6064
|
+
if (!isSafePath(docId)) return null;
|
|
6065
|
+
try {
|
|
6066
|
+
return (0, import_node_fs16.readFileSync)((0, import_node_path15.resolve)(cwd, docId), "utf-8");
|
|
6067
|
+
} catch {
|
|
6068
|
+
return null;
|
|
6069
|
+
}
|
|
6070
|
+
};
|
|
6071
|
+
const persistHumanPatch = (docId, doc) => {
|
|
6072
|
+
if (!isSafePath(docId)) return;
|
|
6073
|
+
writeFileEnsuringDir((0, import_node_path15.resolve)(cwd, docId), serializeAtelier(doc));
|
|
6074
|
+
};
|
|
6075
|
+
bridgeState.store.onChange((id, doc, source) => {
|
|
6076
|
+
if (!shouldBroadcastMutation(source)) return;
|
|
6077
|
+
if (doc === null) return;
|
|
6078
|
+
try {
|
|
6079
|
+
persistHumanPatch(id, doc);
|
|
6080
|
+
} catch {
|
|
6081
|
+
}
|
|
6082
|
+
broadcastToBridge(bridgeClients, {
|
|
6083
|
+
type: "llm:mutation",
|
|
6084
|
+
documentId: id,
|
|
6085
|
+
doc,
|
|
6086
|
+
source
|
|
6087
|
+
});
|
|
6088
|
+
});
|
|
4121
6089
|
const server = await vite.createServer({
|
|
4122
6090
|
root: tmpDir,
|
|
4123
6091
|
server: {
|
|
6092
|
+
host: HOSTNAME,
|
|
4124
6093
|
port,
|
|
4125
6094
|
strictPort: false,
|
|
4126
6095
|
fs: {
|
|
@@ -4131,8 +6100,21 @@ function studioCommand(program2) {
|
|
|
4131
6100
|
{
|
|
4132
6101
|
name: "atelier-api",
|
|
4133
6102
|
configureServer(server2) {
|
|
6103
|
+
const allowedOrigins = /* @__PURE__ */ new Set([
|
|
6104
|
+
`http://localhost:${port}`,
|
|
6105
|
+
`http://127.0.0.1:${port}`
|
|
6106
|
+
]);
|
|
6107
|
+
const MUTATING = /* @__PURE__ */ new Set(["POST", "PUT", "DELETE", "PATCH"]);
|
|
4134
6108
|
server2.middlewares.use((req, res, next) => {
|
|
4135
|
-
const url2 = new URL(req.url ?? "/", `http
|
|
6109
|
+
const url2 = new URL(req.url ?? "/", `http://${HOSTNAME}:${port}`);
|
|
6110
|
+
if (req.method && MUTATING.has(req.method) && url2.pathname.startsWith("/api/")) {
|
|
6111
|
+
const origin = req.headers.origin;
|
|
6112
|
+
if (!origin || !allowedOrigins.has(origin)) {
|
|
6113
|
+
res.statusCode = 403;
|
|
6114
|
+
res.end("Forbidden: cross-origin mutating request rejected");
|
|
6115
|
+
return;
|
|
6116
|
+
}
|
|
6117
|
+
}
|
|
4136
6118
|
if (url2.pathname === "/api/files") {
|
|
4137
6119
|
const atelierFiles2 = findAtelierFiles(cwd);
|
|
4138
6120
|
const entries = atelierFiles2.map((p) => {
|
|
@@ -4154,10 +6136,15 @@ function studioCommand(program2) {
|
|
|
4154
6136
|
res.end("Invalid path");
|
|
4155
6137
|
return;
|
|
4156
6138
|
}
|
|
4157
|
-
const absPath = (0,
|
|
6139
|
+
const absPath = (0, import_node_path15.resolve)(cwd, filePath);
|
|
4158
6140
|
if (req.method === "GET") {
|
|
4159
6141
|
try {
|
|
4160
|
-
const content = (0,
|
|
6142
|
+
const content = (0, import_node_fs16.readFileSync)(absPath, "utf-8");
|
|
6143
|
+
const parsed = parseAtelier(content);
|
|
6144
|
+
if (parsed.success) {
|
|
6145
|
+
bridgeState.store.set(filePath, parsed.data, "system");
|
|
6146
|
+
bridgeState.currentDocId = filePath;
|
|
6147
|
+
}
|
|
4161
6148
|
res.setHeader("Content-Type", "text/plain");
|
|
4162
6149
|
res.end(content);
|
|
4163
6150
|
} catch {
|
|
@@ -4173,11 +6160,17 @@ function studioCommand(program2) {
|
|
|
4173
6160
|
});
|
|
4174
6161
|
req.on("end", () => {
|
|
4175
6162
|
try {
|
|
4176
|
-
(
|
|
6163
|
+
writeFileEnsuringDir(absPath, body);
|
|
6164
|
+
const parsed = parseAtelier(body);
|
|
6165
|
+
if (parsed.success) {
|
|
6166
|
+
bridgeState.store.set(filePath, parsed.data, "human");
|
|
6167
|
+
bridgeState.currentDocId = filePath;
|
|
6168
|
+
}
|
|
4177
6169
|
res.end("OK");
|
|
4178
|
-
} catch {
|
|
6170
|
+
} catch (e) {
|
|
6171
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
4179
6172
|
res.statusCode = 500;
|
|
4180
|
-
res.end(
|
|
6173
|
+
res.end(msg);
|
|
4181
6174
|
}
|
|
4182
6175
|
});
|
|
4183
6176
|
return;
|
|
@@ -4190,19 +6183,20 @@ function studioCommand(program2) {
|
|
|
4190
6183
|
res.end("Invalid path");
|
|
4191
6184
|
return;
|
|
4192
6185
|
}
|
|
4193
|
-
const absPath = (0,
|
|
6186
|
+
const absPath = (0, import_node_path15.resolve)(cwd, filePath);
|
|
4194
6187
|
const chunks = [];
|
|
4195
6188
|
req.on("data", (chunk) => {
|
|
4196
6189
|
chunks.push(chunk);
|
|
4197
6190
|
});
|
|
4198
6191
|
req.on("end", () => {
|
|
4199
6192
|
try {
|
|
4200
|
-
(0,
|
|
4201
|
-
(0,
|
|
6193
|
+
(0, import_node_fs16.mkdirSync)((0, import_node_path15.dirname)(absPath), { recursive: true });
|
|
6194
|
+
(0, import_node_fs16.writeFileSync)(absPath, Buffer.concat(chunks));
|
|
4202
6195
|
res.end("OK");
|
|
4203
|
-
} catch {
|
|
6196
|
+
} catch (e) {
|
|
6197
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
4204
6198
|
res.statusCode = 500;
|
|
4205
|
-
res.end(
|
|
6199
|
+
res.end(msg);
|
|
4206
6200
|
}
|
|
4207
6201
|
});
|
|
4208
6202
|
return;
|
|
@@ -4220,6 +6214,51 @@ function studioCommand(program2) {
|
|
|
4220
6214
|
logLevel: "warn"
|
|
4221
6215
|
});
|
|
4222
6216
|
await server.listen();
|
|
6217
|
+
const httpServer = server.httpServer;
|
|
6218
|
+
if (httpServer) {
|
|
6219
|
+
const wssBridge = new import_ws.WebSocketServer({ noServer: true });
|
|
6220
|
+
const wssMcp = new import_ws.WebSocketServer({ noServer: true });
|
|
6221
|
+
httpServer.on("upgrade", (req, socket, head) => {
|
|
6222
|
+
const url2 = new URL(req.url ?? "/", `http://${HOSTNAME}:${port}`);
|
|
6223
|
+
if (url2.pathname !== "/bridge" && url2.pathname !== "/mcp") return;
|
|
6224
|
+
const origin = req.headers.origin;
|
|
6225
|
+
const originOk = url2.pathname === "/mcp" ? isAllowedMcpOrigin(origin, port) : isAllowedOrigin(origin, port);
|
|
6226
|
+
if (!originOk) {
|
|
6227
|
+
socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
|
|
6228
|
+
socket.destroy();
|
|
6229
|
+
return;
|
|
6230
|
+
}
|
|
6231
|
+
if (url2.pathname === "/bridge") {
|
|
6232
|
+
wssBridge.handleUpgrade(req, socket, head, (ws) => {
|
|
6233
|
+
wssBridge.emit("connection", ws, req);
|
|
6234
|
+
});
|
|
6235
|
+
return;
|
|
6236
|
+
}
|
|
6237
|
+
wssMcp.handleUpgrade(req, socket, head, (ws) => {
|
|
6238
|
+
wssMcp.emit("connection", ws, req);
|
|
6239
|
+
});
|
|
6240
|
+
});
|
|
6241
|
+
wssBridge.on("connection", (ws) => {
|
|
6242
|
+
attachBridgeClient(
|
|
6243
|
+
ws,
|
|
6244
|
+
bridgeState,
|
|
6245
|
+
bridgeClients,
|
|
6246
|
+
loadDocFromDisk,
|
|
6247
|
+
persistHumanPatch
|
|
6248
|
+
);
|
|
6249
|
+
});
|
|
6250
|
+
wssMcp.on("connection", (ws) => {
|
|
6251
|
+
const { server: mcpServer } = (0, import_atelier_mcp.createServer)(bridgeState.store);
|
|
6252
|
+
const transport = new import_atelier_mcp.WebSocketServerTransport(ws);
|
|
6253
|
+
mcpServer.connect(transport).catch((err) => {
|
|
6254
|
+
console.error("MCP-over-WS connect failed:", err);
|
|
6255
|
+
try {
|
|
6256
|
+
ws.close();
|
|
6257
|
+
} catch {
|
|
6258
|
+
}
|
|
6259
|
+
});
|
|
6260
|
+
});
|
|
6261
|
+
}
|
|
4223
6262
|
const resolvedUrl = server.resolvedUrls?.local[0] ?? `http://localhost:${port}`;
|
|
4224
6263
|
const url = resolvedUrl;
|
|
4225
6264
|
console.log(` Server running at: ${url}`);
|
|
@@ -4231,13 +6270,13 @@ function studioCommand(program2) {
|
|
|
4231
6270
|
console.log(` Press Ctrl+C to stop
|
|
4232
6271
|
`);
|
|
4233
6272
|
if (options.open) {
|
|
4234
|
-
(0,
|
|
6273
|
+
(0, import_node_child_process4.exec)(`open "${url}"`);
|
|
4235
6274
|
}
|
|
4236
6275
|
const cleanup = () => {
|
|
4237
6276
|
console.log("\nShutting down...");
|
|
4238
6277
|
server.close();
|
|
4239
6278
|
try {
|
|
4240
|
-
(0,
|
|
6279
|
+
(0, import_node_fs16.rmSync)(tmpDir, { recursive: true, force: true });
|
|
4241
6280
|
} catch {
|
|
4242
6281
|
}
|
|
4243
6282
|
process.exit(0);
|
|
@@ -4253,11 +6292,20 @@ var import_meta2 = {};
|
|
|
4253
6292
|
var program = new import_commander.Command();
|
|
4254
6293
|
program.name("atelier").description("Atelier animation CLI").version((0, import_node_module.createRequire)(import_meta2.url)("../package.json").version);
|
|
4255
6294
|
validateCommand(program);
|
|
6295
|
+
lintCommand(program);
|
|
6296
|
+
trimCommand(program);
|
|
6297
|
+
transcribeCommand(program);
|
|
6298
|
+
transcriptCommand(program);
|
|
6299
|
+
captionsCommand(program);
|
|
6300
|
+
recipeCommand(program);
|
|
6301
|
+
applyRecipeCommand(program);
|
|
6302
|
+
carouselCommand(program);
|
|
4256
6303
|
infoCommand(program);
|
|
4257
6304
|
stillCommand(program);
|
|
4258
6305
|
renderCommand(program);
|
|
4259
6306
|
exportSvgCommand(program);
|
|
4260
6307
|
exportLottieCommand(program);
|
|
6308
|
+
exportImageCommand(program);
|
|
4261
6309
|
assetsCommand(program);
|
|
4262
6310
|
variablesCommand(program);
|
|
4263
6311
|
studioCommand(program);
|