feat(cli): add blank template option, smart defaults for video vs audio

- Blank template: minimal scaffolding (root composition, video, audio,
  GSAP timeline) with __VIDEO_SRC__ and __VIDEO_DURATION__ placeholders
- Template defaults: video uploads default to "blank" (user brings
  their own content), audio-only defaults to "warm-grain" (motion
  graphics template since there's no video to show)
- Audio-only projects now tracked with isAudioOnly flag

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Vance Ingalls
2026-03-26 18:03:36 +00:00
committed by Vance Ingalls
co-authored by Claude Opus 4.6
parent dce10e3e3b
commit f6def4295e
5 changed files with 145 additions and 6 deletions
+1 -1
View File
@@ -14,7 +14,7 @@
"build": "bun run build:studio && tsup && bun run build:runtime && bun run build:copy",
"build:studio": "cd ../studio && bun run build",
"build:runtime": "tsx scripts/build-runtime.ts",
"build:copy": "mkdir -p dist/studio dist/docs dist/templates && cp -r ../studio/dist/* dist/studio/ && cp -r src/templates/warm-grain src/templates/play-mode src/templates/swiss-grid src/templates/vignelli dist/templates/ && (cp src/docs/*.md dist/docs/ 2>/dev/null || true)",
"build:copy": "mkdir -p dist/studio dist/docs dist/templates && cp -r ../studio/dist/* dist/studio/ && cp -r src/templates/blank src/templates/warm-grain src/templates/play-mode src/templates/swiss-grid src/templates/vignelli dist/templates/ && (cp src/docs/*.md dist/docs/ 2>/dev/null || true)",
"typecheck": "tsc --noEmit"
},
"dependencies": {
+6 -4
View File
@@ -559,6 +559,7 @@ export default defineCommand({
let localVideoName: string | undefined;
let sourceFilePath: string | undefined;
let videoDuration: number | undefined;
let isAudioOnly = false;
if (videoFlag) {
const videoPath = resolve(videoFlag);
@@ -612,8 +613,8 @@ export default defineCommand({
localVideoName = result.localVideoName;
videoDuration = result.meta.durationSeconds;
} else {
// Audio file — copy to assets/
// Audio file — copy to project root
isAudioOnly = true;
copyFileSync(filePath, resolve(destDir, basename(filePath)));
clack.log.info(`Audio copied to ${c.accent(basename(filePath))}`);
}
@@ -662,7 +663,8 @@ export default defineCommand({
}
}
// 3. Pick template — single list for all templates
// 3. Pick template — default depends on media type
const defaultTemplate = isAudioOnly ? "warm-grain" : "blank";
const templateResult = await clack.select({
message: "Pick a template",
options: TEMPLATES.map((t) => ({
@@ -670,7 +672,7 @@ export default defineCommand({
label: t.label,
hint: t.hint,
})),
initialValue: TEMPLATES[0]?.id,
initialValue: defaultTemplate as TemplateId,
});
if (clack.isCancel(templateResult)) {
clack.cancel("Setup cancelled.");
@@ -0,0 +1,92 @@
<template id="captions-template">
<div
data-composition-id="captions"
data-width="1920"
data-height="1080"
data-duration="__VIDEO_DURATION__"
>
<div id="captions-container"></div>
<style>
[data-composition-id="captions"] {
width: 1920px;
height: 1080px;
pointer-events: none;
}
[data-composition-id="captions"] #captions-container {
position: absolute;
bottom: 100px;
left: 50%;
transform: translateX(-50%);
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 150px;
}
.caption-group {
position: absolute;
opacity: 0;
font-family: "Inter", sans-serif;
font-size: 48px;
font-weight: 700;
color: #ffffff;
text-shadow:
0 2px 8px rgba(0, 0, 0, 0.8),
0 0 2px rgba(0, 0, 0, 0.9);
white-space: nowrap;
}
</style>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
(function () {
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
const script = [];
if (script.length === 0) {
window.__timelines["captions"] = tl;
return;
}
const container = document.getElementById("captions-container");
// Group words into lines (max 5 words per line)
const lines = [];
for (let i = 0; i < script.length; i += 5) {
const lineWords = script.slice(i, i + 5);
lines.push({
text: lineWords.map((w) => w.text).join(" "),
start: lineWords[0].start,
end: lineWords[lineWords.length - 1].end,
});
}
lines.forEach((line, index) => {
const el = document.createElement("div");
el.className = "caption-group";
el.textContent = line.text;
container.appendChild(el);
tl.fromTo(
el,
{ opacity: 0, y: 20 },
{ opacity: 1, y: 0, duration: 0.3, ease: "power3.out" },
line.start,
);
const hideTime =
index < lines.length - 1 ? Math.min(line.end, lines[index + 1].start) : line.end;
tl.to(el, { opacity: 0, y: -10, duration: 0.25, ease: "power2.in" }, hideTime - 0.25);
});
window.__timelines["captions"] = tl;
})();
</script>
</div>
</template>
@@ -0,0 +1,44 @@
<div
id="root"
data-composition-id="main"
data-start="0"
data-duration="__VIDEO_DURATION__"
data-width="1920"
data-height="1080"
>
<video
id="a-roll"
src="__VIDEO_SRC__"
muted
playsinline
data-start="0"
data-duration="__VIDEO_DURATION__"
data-track-index="0"
></video>
<audio
id="a-roll-audio"
src="__VIDEO_SRC__"
data-start="0"
data-duration="__VIDEO_DURATION__"
data-track-index="2"
data-volume="1"
></audio>
<div
id="captions-comp"
data-composition-id="captions"
data-composition-src="compositions/captions.html"
data-start="0"
data-duration="__VIDEO_DURATION__"
data-track-index="3"
data-width="1920"
data-height="1080"
></div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
window.__timelines = window.__timelines || {};
const tl = gsap.timeline({ paused: true });
window.__timelines["main"] = tl;
</script>
</div>
+2 -1
View File
@@ -1,4 +1,4 @@
export type TemplateId = "warm-grain" | "play-mode" | "swiss-grid" | "vignelli";
export type TemplateId = "blank" | "warm-grain" | "play-mode" | "swiss-grid" | "vignelli";
export interface TemplateOption {
id: TemplateId;
@@ -7,6 +7,7 @@ export interface TemplateOption {
}
export const TEMPLATES: TemplateOption[] = [
{ id: "blank", label: "Blank", hint: "Empty composition — just the scaffolding" },
{ id: "warm-grain", label: "Warm Grain", hint: "Cream aesthetic with grain texture" },
{ id: "play-mode", label: "Play Mode", hint: "Playful elastic animations" },
{ id: "swiss-grid", label: "Swiss Grid", hint: "Structured grid layout" },