mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-05 05:37:09 +00:00
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:
committed by
Vance Ingalls
co-authored by
Claude Opus 4.6
parent
dce10e3e3b
commit
f6def4295e
@@ -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": {
|
||||
|
||||
@@ -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>
|
||||
@@ -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" },
|
||||
|
||||
Reference in New Issue
Block a user