diff --git a/src/remotion/ShortsVideo.tsx b/src/remotion/ShortsVideo.tsx
index 38da25c..5400f97 100644
--- a/src/remotion/ShortsVideo.tsx
+++ b/src/remotion/ShortsVideo.tsx
@@ -9,6 +9,11 @@ import {
KineticTitle,
StatCounter,
SlideMotion,
+ TerminalScene,
+ CompareScene,
+ BulletScene,
+ BangScene,
+ MiniKaraoke,
} from "./parts";
import { TweetCard, ThreadCard, NewsCard } from "./cards";
@@ -21,7 +26,7 @@ export type Segment = {
label?: string;
sentences?: Sentence[];
/** optional explicit scene override */
- scene?: "title" | "stat" | "caption" | "cta" | "tweet" | "thread" | "news";
+ scene?: "title" | "stat" | "caption" | "cta" | "tweet" | "thread" | "news" | "terminal" | "compare" | "fix" | "bullets" | "bang";
stat?: { value: string; label: string };
/** source content (tweet/thread/news) — rendered as a visual card scene */
source?: {
@@ -72,12 +77,18 @@ function extractStat(text: string): { value: string; label: string } | null {
function pickScene(seg: Segment, index: number): Segment["scene"] | "caption" {
if (seg.scene) return seg.scene;
const label = (seg.label || "").toUpperCase();
- if (label === "HOOK" || label === "ON SCREEN") return "title";
+ // AI-tips scenes: terminal hook, mistake→compare(✗), insight→bullet list,
+ // fix→compare(✓), bang for conflict.
+ if (label === "THE MISTAKE" || label === "WHY IT FAILS") return "compare";
+ if (label === "THE FIX") return "fix";
+ if (label === "THE INSIGHT") return "bullets";
+ if (label === "HOOK") return "terminal";
if (label === "CTA") return "cta";
if (seg.source) return seg.source.type as "tweet" | "thread" | "news";
if (seg.stat) return "stat";
const stat = extractStat(seg.text);
if (stat) return "stat";
+ if (label === "THE CONFLICT") return "bang";
if (index % 3 === 2) return "title";
return "caption";
}
@@ -97,6 +108,60 @@ const Scene: React.FC<{
: [{ w: text, start: 0, end: duration / 30 }];
const src = seg.source;
+ // creative scenes: big visual + compact caption strip below
+ if (scene === "terminal") {
+ return (
+
+
+
+ {label ? : null}
+
+
+
+
+
+ );
+ }
+ if (scene === "compare" || scene === "fix") {
+ return (
+
+
+
+ {label ? : null}
+
+
+
+
+
+ );
+ }
+ if (scene === "bullets") {
+ return (
+
+
+
+ {label ? : null}
+
+
+
+
+
+ );
+ }
+ if (scene === "bang") {
+ return (
+
+
+
+ {label ? : null}
+
+
+
+
+
+ );
+ }
+
return (
diff --git a/src/remotion/parts.tsx b/src/remotion/parts.tsx
index 73f2b4b..74de2bd 100644
--- a/src/remotion/parts.tsx
+++ b/src/remotion/parts.tsx
@@ -355,4 +355,329 @@ export const SlideMotion: React.FC<{ children: React.ReactNode }> = ({ children
{children}
);
+};
+
+/* ═══════════════════════════════════════════════════════════════════════
+ CREATIVE SCENES — visual variety beyond the caption card.
+ All monochrome flat, transform/opacity only (no blur/SVG filters).
+ ═══════════════════════════════════════════════════════════════════════ */
+
+/** Monospace terminal typeface. */
+const FONT_MONO = "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
+
+/** Line icons drawn with strokes (animate in via opacity/offset). */
+export const LineIcon: React.FC<{
+ kind: "check" | "cross" | "node" | "bolt";
+ size?: number;
+ delay?: number;
+}> = ({ kind, size = 64, delay = 0 }) => {
+ const frame = useCurrentFrame();
+ const { fps } = useVideoConfig();
+ const pop = spring({ frame: frame - delay, fps, config: { damping: 16, stiffness: 120 } });
+ const s = size;
+ const stroke = "currentColor";
+ return (
+
+ );
+};
+
+/** Terminal scene — "$" prompt + typed commands, blinking cursor (AI-agent vibe). */
+export const TerminalScene: React.FC<{ text: string; size?: number }> = ({ text, size = 40 }) => {
+ const frame = useCurrentFrame();
+ const { fps } = useVideoConfig();
+ // Split into command line(s) + output lines (roughly 4-6 words each)
+ const words = text.split(" ");
+ const lines: string[] = [];
+ let cur = "";
+ for (const w of words) {
+ if ((cur + " " + w).trim().split(" ").length > 5) { lines.push(cur.trim()); cur = w; }
+ else cur = (cur + " " + w).trim();
+ }
+ if (cur) lines.push(cur.trim());
+ const cmd = lines[0] || "$ agent --fix";
+ const output = lines.slice(1);
+ // typing effect for command
+ const typeDur = Math.max(6, Math.floor(fps * 0.8)); // frames to type full command
+ const chars = Math.min(cmd.length, Math.floor((frame / typeDur) * cmd.length));
+ const typed = cmd.slice(0, chars);
+ const blink = frame % (fps * 1.1) < fps * 0.55;
+ const pop = spring({ frame, fps, config: { damping: 15, stiffness: 100 } });
+ return (
+
+
+ {[0, 1, 2].map((i) => (
+
+ ))}
+
+
+ $
+ {typed}
+ ▌
+
+ {output.map((l, i) => {
+ const o = Math.max(0, Math.min(1, (frame - typeDur - i * 10) / 12));
+ return (
+
+ {l}
+
+ );
+ })}
+
+ );
+};
+
+/** DON'T (✗) vs DO (✓) — split compare panel. */
+export const CompareScene: React.FC<{ text: string; side: "bad" | "good"; size?: number }> = ({ text, side, size = 38 }) => {
+ const frame = useCurrentFrame();
+ const { fps } = useVideoConfig();
+ const pop = spring({ frame, fps, config: { damping: 15, stiffness: 100 } });
+ const words = text.split(" ");
+ const chunk = Math.max(3, Math.ceil(words.length / 2));
+ const halves = [words.slice(0, chunk).join(" "), words.slice(chunk).join(" ")];
+ const good = side === "good";
+ return (
+
+ );
+};
+
+/** Numbered bullet list (for insights: "1. one idea 2. …") — animated per bullet. */
+export const BulletScene: React.FC<{ text: string; size?: number }> = ({ text, size = 40 }) => {
+ const frame = useCurrentFrame();
+ const { fps } = useVideoConfig();
+ const words = text.split(" ");
+ const per = Math.max(3, Math.ceil(words.length / 3));
+ const bullets = [
+ words.slice(0, per).join(" "),
+ words.slice(per, per * 2).join(" "),
+ words.slice(per * 2).join(" "),
+ ].filter(Boolean);
+ return (
+
+ {bullets.map((b, i) => {
+ const start = i * 10;
+ const o = Math.max(0, Math.min(1, (frame - start) / 12));
+ const y = (1 - o) * 30;
+ return (
+
+
+ {i + 1}
+
+
+ {b}
+
+
+ );
+ })}
+
+ );
+};
+
+/** Bang scene — one keyword bursts center-stage with radial rays (the twist). */
+export const BangScene: React.FC<{ text: string }> = ({ text }) => {
+ const frame = useCurrentFrame();
+ const { fps } = useVideoConfig();
+ const words = text.split(" ");
+ const keyword = words.length > 4 ? words.slice(0, 3).join(" ") : text;
+ const rest = words.length > 4 ? words.slice(3).join(" ") : "";
+ const boom = spring({ frame, fps, config: { damping: 12, stiffness: 90 } });
+ const rays = 12;
+ return (
+
+ {/* radial rays */}
+
+
+ {keyword}
+
+ {rest && (
+
+ {rest}
+
+ )}
+
+ );
+};
+
+/** Compact karaoke caption strip — subtitle support below the big visual. */
+export const MiniKaraoke: React.FC<{
+ sentences: { w: string; start: number; end: number }[];
+ size?: number;
+}> = ({ sentences, size = 26 }) => {
+ const frame = useCurrentFrame();
+ const { fps } = useVideoConfig();
+ const now = frame / fps;
+ let activeIdx = 0;
+ for (let i = 0; i < sentences.length; i++) {
+ if (now >= sentences[i].start) { activeIdx = i; break; }
+ }
+ return (
+
+ {sentences.map((s, i) => (
+
+ {s.w}
+ {i < sentences.length - 1 ? " " : ""}
+
+ ))}
+
+ );
};
\ No newline at end of file