Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
30 KiB
Advanced Remotion Templates Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Extend the caption animation system with 4 new highlight styles, 2 new segment transitions, 3 new animation fields, and ship 2 system presets ("Шортс" and "Подкаст").
Architecture: Additive changes only — extend existing CaptionStyleSchema with new enum values and fields, implement new animation rendering in Captions.tsx using Remotion primitives, seed 2 new system presets via Alembic migration, and add new form controls to the frontend StyleEditor.
Tech Stack: Remotion (interpolate(), spring()), ElysiaJS (Elysia t.* validation), FastAPI + Pydantic + Alembic, Next.js + react-hook-form
Spec: docs/superpowers/specs/2026-03-21-advanced-remotion-templates-design.md
File Map
Remotion Service (remotion_service/)
| Action | File | Responsibility |
|---|---|---|
| Modify | server/types/CaptionStyleSchema.ts |
Add new enum values + 3 new fields to Elysia validation schema |
| Modify | src/types/caption_style.d.ts |
Mirror TypeScript type changes |
| Modify | src/components/Captions.tsx |
Implement 4 new highlight renderers, 2 new transitions, word entrance logic, rotation, text-transform |
Backend (cofee_backend/)
| Action | File | Responsibility |
|---|---|---|
| Modify | cpv3/modules/captions/schemas.py |
Extend Pydantic CaptionAnimationStyle with new Literal values + 3 fields |
| Create | alembic/versions/e6f7a8b9c0d1_seed_shorts_podcast_presets.py |
Seed 2 new system presets |
Frontend (cofee_frontend/)
| Action | File | Responsibility |
|---|---|---|
| Modify | src/features/project/CaptionSettingsStep/StyleEditor.tsx |
Add new select options + 3 new form fields to AnimationFields |
Task 1: Extend Remotion Schema & Types
Files:
-
Modify:
remotion_service/server/types/CaptionStyleSchema.ts(lines 30-47) -
Modify:
remotion_service/src/types/caption_style.d.ts(lines 20-26) -
Step 1: Update Elysia validation schema
In remotion_service/server/types/CaptionStyleSchema.ts, replace the CaptionAnimationStyle object (lines 30-47) with:
export const CaptionAnimationStyle = t.Object({
highlight_style: t.Union(
[
t.Literal("color"),
t.Literal("scale"),
t.Literal("underline"),
t.Literal("color_scale"),
t.Literal("pop_in"),
t.Literal("karaoke"),
t.Literal("bounce"),
t.Literal("glow_pulse"),
],
{ default: "color" },
),
highlight_scale: t.Number({ default: 1.1 }),
segment_transition: t.Union(
[
t.Literal("fade"),
t.Literal("slide"),
t.Literal("none"),
t.Literal("zoom_in"),
t.Literal("drop_in"),
],
{ default: "fade" },
),
fade_duration_frames: t.Number({ default: 3 }),
animation_speed: t.Number({ default: 1.0 }),
word_entrance: t.Union(
[t.Literal("none"), t.Literal("pop"), t.Literal("typewriter")],
{ default: "none" },
),
highlight_rotation_deg: t.Number({ default: 0 }),
text_transform: t.Union(
[t.Literal("none"), t.Literal("uppercase"), t.Literal("lowercase")],
{ default: "none" },
),
});
- Step 2: Update TypeScript type definitions
In remotion_service/src/types/caption_style.d.ts, replace the CaptionAnimationStyle type (lines 20-26) with:
export type CaptionAnimationStyle = {
highlight_style:
| "color"
| "scale"
| "underline"
| "color_scale"
| "pop_in"
| "karaoke"
| "bounce"
| "glow_pulse";
highlight_scale: number;
segment_transition: "fade" | "slide" | "none" | "zoom_in" | "drop_in";
fade_duration_frames: number;
animation_speed: number;
word_entrance: "none" | "pop" | "typewriter";
highlight_rotation_deg: number;
text_transform: "none" | "uppercase" | "lowercase";
};
- Step 3: Type-check Remotion service
Run: cd remotion_service && bunx tsc --noEmit
Expected: PASS — no type errors (existing code uses only the old enum values, which are still present)
- Step 4: Commit
cd remotion_service
git add server/types/CaptionStyleSchema.ts src/types/caption_style.d.ts
git commit -m "feat(remotion): extend CaptionAnimationStyle schema with new highlight styles, transitions, and fields"
Task 2: Implement New Highlight Styles in Captions.tsx
Files:
- Modify:
remotion_service/src/components/Captions.tsx(lines 57-136 —StyledWordcomponent)
This is the largest task. We modify the StyledWord component to handle 4 new highlight styles. The current isCurrent block (lines 86-133) handles color, scale, underline, color_scale. We add pop_in, karaoke, bounce, glow_pulse.
- Step 1: Add spring import
At the top of remotion_service/src/components/Captions.tsx, add spring to the Remotion import (line 2):
import { interpolate, spring, useVideoConfig } from "remotion";
- Step 2: Add
useVideoConfigto StyledWord
Inside StyledWord (line 69), add fps extraction right after the destructure:
const { fps } = useVideoConfig();
This is needed for spring() calls which require fps.
- Step 3: Implement
pop_inhighlight style
In StyledWord, inside the if (isCurrent) block (after the existing underline branch at line 132), add:
if (animation.highlight_style === "pop_in") {
const wordDuration = wordFrameTime.end - wordFrameTime.start;
const scale =
wordDuration > MIN_INTERPOLATE_SPAN * 2
? spring({
fps,
frame: currentFrame - wordFrameTime.start,
config: { damping: 12, stiffness: 200 },
durationInFrames: Math.min(Math.ceil(wordDuration / 2), 15),
})
: 1;
const finalScale = interpolate(scale, [0, 1], [0, animation.highlight_scale]);
baseStyle.transform = `scale(${finalScale})`;
}
- Step 4: Implement
karaokehighlight style
After the pop_in block, add:
if (animation.highlight_style === "karaoke") {
const wordDuration = wordFrameTime.end - wordFrameTime.start;
const progress =
wordDuration > MIN_INTERPOLATE_SPAN
? interpolate(
currentFrame,
[wordFrameTime.start, wordFrameTime.end],
[0, 100],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" },
)
: 100;
baseStyle.background = `linear-gradient(to right, ${textStyle.highlight_color} ${progress}%, ${textStyle.text_color} ${progress}%)`;
baseStyle.WebkitBackgroundClip = "text";
baseStyle.WebkitTextFillColor = "transparent";
baseStyle.backgroundClip = "text";
// Override the color set above — karaoke uses gradient instead
baseStyle.color = undefined;
}
- Step 5: Implement
bouncehighlight style
After the karaoke block, add:
if (animation.highlight_style === "bounce") {
const wordDuration = wordFrameTime.end - wordFrameTime.start;
const scale =
wordDuration > MIN_INTERPOLATE_SPAN * 2
? spring({
fps,
frame: currentFrame - wordFrameTime.start,
config: { damping: 8, stiffness: 180 },
durationInFrames: Math.min(Math.ceil(wordDuration), 20),
})
: 1;
const finalScale = interpolate(scale, [0, 1], [1, animation.highlight_scale]);
baseStyle.transform = `scale(${finalScale})`;
}
- Step 6: Implement
glow_pulsehighlight style
After the bounce block, add:
if (animation.highlight_style === "glow_pulse") {
const wordDuration = wordFrameTime.end - wordFrameTime.start;
const pulse =
wordDuration > MIN_INTERPOLATE_SPAN * 2
? interpolate(
currentFrame,
[
wordFrameTime.start,
wordFrameTime.start + wordDuration * 0.25,
wordFrameTime.start + wordDuration * 0.5,
wordFrameTime.start + wordDuration * 0.75,
wordFrameTime.end,
],
[4, 12, 4, 12, 4],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" },
)
: 8;
baseStyle.textShadow = `0 0 ${pulse}px ${textStyle.highlight_color}, 0 0 ${pulse * 2}px ${textStyle.highlight_color}`;
}
- Step 7: Add
highlight_rotation_degsupport
After all highlight style branches (still inside if (isCurrent)), add rotation support:
if (animation.highlight_rotation_deg > 0) {
const existingTransform = baseStyle.transform || "";
baseStyle.transform = `${existingTransform} rotate(${animation.highlight_rotation_deg}deg)`.trim();
}
- Step 8: Type-check
Run: cd remotion_service && bunx tsc --noEmit
Expected: PASS
- Step 9: Commit
cd remotion_service
git add src/components/Captions.tsx
git commit -m "feat(remotion): implement pop_in, karaoke, bounce, glow_pulse highlight styles + rotation"
Task 3: Implement Word Entrance, Text Transform, and New Transitions
Files:
-
Modify:
remotion_service/src/components/Captions.tsx -
Step 1: Add
useVideoConfigto the Captions component
At the top of the Captions component (around line 186), add fps extraction for spring() calls in transitions:
const { fps: videoFps } = useVideoConfig();
This is needed by drop_in transition (Step 4) and must be declared before use.
- Step 2: Implement
word_entrancein StyledWord
In StyledWord, BEFORE the if (isCurrent) block (around line 86), add word entrance logic. This controls how words appear before they're spoken. The spring also applies when the word is isCurrent (just started being spoken) so the pop-in is smooth regardless of highlight style:
// Word entrance: controls visibility/scale of words before their start frame
const entrance = style.animation.word_entrance ?? "none";
if (entrance !== "none" && currentFrame < wordFrameTime.start) {
// Word hasn't been spoken yet — hide it
if (entrance === "pop") {
baseStyle.transform = "scale(0)";
baseStyle.opacity = 0;
} else if (entrance === "typewriter") {
baseStyle.opacity = 0;
}
} else if (entrance === "pop" && currentFrame >= wordFrameTime.start) {
// Word has been spoken (or is being spoken right now) — spring it in
const framesSinceStart = currentFrame - wordFrameTime.start;
const popScale = spring({
fps,
frame: framesSinceStart,
config: { damping: 12, stiffness: 200 },
durationInFrames: 10,
});
// Set as base transform — highlight styles will append to it if needed
baseStyle.transform = `scale(${popScale})`;
}
Note: The pop spring applies to BOTH isCurrent and past words. When isCurrent, the highlight style block below may overwrite baseStyle.transform (e.g., bounce sets its own scale). This is intentional — the highlight animation takes precedence once the word is active. For highlight styles that don't set transform (like color or karaoke), the pop spring provides the entrance animation.
- Step 3: Implement
zoom_inanddrop_insegment transitions
In the Captions component, after the existing slide transition block (around line 247), add both new transitions and a scale variable:
let scale = 1;
if (transition === "zoom_in" && !hasShortSegment) {
opacity = interpolate(
currentFrame,
hasFadePlateau
? [start, fadeIn, fadeOut, end]
: [start, middleFrame, end],
hasFadePlateau ? [0, 1, 1, 0] : [0, 1, 0],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" },
);
scale = interpolate(
currentFrame,
hasFadePlateau
? [start, fadeIn, fadeOut, end]
: [start, middleFrame, end],
hasFadePlateau ? [0.8, 1, 1, 1.2] : [0.8, 1, 1.2],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" },
);
}
if (transition === "drop_in" && !hasShortSegment) {
opacity = interpolate(
currentFrame,
hasFadePlateau
? [start, fadeIn, fadeOut, end]
: [start, middleFrame, end],
hasFadePlateau ? [0, 1, 1, 0] : [0, 1, 0],
{ extrapolateLeft: "clamp", extrapolateRight: "clamp" },
);
const dropSpring = spring({
fps: videoFps,
frame: currentFrame - start,
config: { damping: 10, stiffness: 150 },
durationInFrames: Math.min(fadeDuration * 3, 20),
});
// Spring goes 0→1, we want -50→0
translateY = interpolate(dropSpring, [0, 1], [-50, 0]);
}
Note: videoFps comes from useVideoConfig() added in Step 1.
- Step 4: Implement
text_transformand updatesegmentStyle.transform
In the Captions component (line 250, inside the inline style mode block), rebuild segmentStyle to include textTransform and the composite transform (accounting for translateY + scale):
const textTransformValue = styleConfig.animation.text_transform ?? "none";
const segmentStyle: React.CSSProperties = {
opacity,
transform: [
translateY !== 0 ? `translateY(${translateY}px)` : "",
scale !== 1 ? `scale(${scale})` : "",
].filter(Boolean).join(" ") || undefined,
display: "flex",
flexDirection: "column",
alignItems: "center",
textAlign: "center",
width: "100%",
padding: background.bg_padding_px,
background: background.bg_color,
borderRadius: background.bg_border_radius_px,
textTransform: textTransformValue !== "none" ? textTransformValue : undefined,
};
- Step 5: Update the CSS theme mode segment div to include scale
In the CSS theme mode return (line 291), update the style to use the same composite transform:
return (
<div
className="segment"
style={{
opacity,
transform: [
translateY !== 0 ? `translateY(${translateY}px)` : "",
scale !== 1 ? `scale(${scale})` : "",
].filter(Boolean).join(" ") || undefined,
}}
>
- Step 6: Type-check
Run: cd remotion_service && bunx tsc --noEmit
Expected: PASS
- Step 7: Visually verify in Remotion Studio
Run: cd remotion_service && bun run dev
Open Remotion Studio in browser. Test each new animation by modifying the composition props in the studio UI:
- Set
highlight_styleto each of:pop_in,karaoke,bounce,glow_pulse - Set
segment_transitionto each of:zoom_in,drop_in - Set
word_entrancetopopandtypewriter - Set
text_transformtouppercase - Set
highlight_rotation_degto3
Verify each renders without errors and the visual effect matches the spec description.
- Step 8: Commit
cd remotion_service
git add src/components/Captions.tsx
git commit -m "feat(remotion): implement word entrance, text transform, zoom_in and drop_in transitions"
Task 4: Dynamic Font Loading
Files:
- Modify:
remotion_service/src/components/Captions.tsx(lines 1-12 — imports and font loading)
Currently, only Lobster is loaded (line 3 + line 12). The new presets use Montserrat and Inter. We need to dynamically load the correct font based on styleConfig.text.font_family.
- Step 1: Replace static font loading with dynamic loading
Replace lines 1-12 of Captions.tsx:
import React from "react";
import { interpolate, spring, useVideoConfig } from "remotion";
import { loadFont as loadLobster } from "@remotion/google-fonts/Lobster";
import { loadFont as loadInter } from "@remotion/google-fonts/Inter";
import { loadFont as loadMontserrat } from "@remotion/google-fonts/Montserrat";
import { loadFont as loadRoboto } from "@remotion/google-fonts/Roboto";
import { loadFont as loadOpenSans } from "@remotion/google-fonts/OpenSans";
import {
LineWithFrames,
SegmentWithFrames,
WordWithFrames,
} from "@/types/transcription";
import { CaptionStyleConfig } from "@/types/caption_style";
import { useTheme } from "@/hooks/useTheme";
// Load all supported fonts — Remotion deduplicates, only loads what's used
loadLobster();
loadInter();
loadMontserrat();
loadRoboto();
loadOpenSans();
Note: Remotion's loadFont() is idempotent and only triggers one network request per font. Loading all 5 is safe and keeps the component simple. The alternative (dynamic loading based on styleConfig) adds complexity for no real benefit since all 5 fonts are small.
- Step 2: Install missing font packages (if needed)
Run: cd remotion_service && bunx tsc --noEmit
If any @remotion/google-fonts/* imports fail, the packages are already included in @remotion/google-fonts — they're subpath exports, not separate packages. If there's an error, check:
Run: cd remotion_service && bun run dev
The fonts should load in the Remotion Studio preview.
- Step 3: Commit
cd remotion_service
git add src/components/Captions.tsx
git commit -m "feat(remotion): load Inter, Montserrat, Roboto, OpenSans fonts alongside Lobster"
Task 5: Extend Backend Schema
Files:
-
Modify:
cofee_backend/cpv3/modules/captions/schemas.py(lines 37-42) -
Step 1: Update CaptionAnimationStyle Pydantic model
Replace lines 37-42 in cofee_backend/cpv3/modules/captions/schemas.py:
class CaptionAnimationStyle(Schema):
highlight_style: Literal[
"color", "scale", "underline", "color_scale",
"pop_in", "karaoke", "bounce", "glow_pulse",
] = "color"
highlight_scale: float = 1.1
segment_transition: Literal["fade", "slide", "none", "zoom_in", "drop_in"] = "fade"
fade_duration_frames: int = 3
animation_speed: float = 1.0
word_entrance: Literal["none", "pop", "typewriter"] = "none"
highlight_rotation_deg: float = 0
text_transform: Literal["none", "uppercase", "lowercase"] = "none"
- Step 2: Lint and type-check
Run: cd cofee_backend && uv run ruff check cpv3/modules/captions/schemas.py && uv run ruff format cpv3/modules/captions/schemas.py
Expected: PASS or auto-formatted
- Step 3: Verify existing tests still pass
Run: cd cofee_backend && uv run pytest tests/integration/ -x -q 2>&1 | tail -5
Expected: All existing tests pass (new fields have defaults, so backward compatible)
- Step 4: Commit
cd cofee_backend
git add cpv3/modules/captions/schemas.py
git commit -m "feat(backend): extend CaptionAnimationStyle with new highlight styles, transitions, and fields"
Task 6: Seed New System Presets (Alembic Migration)
Files:
-
Create:
cofee_backend/alembic/versions/e6f7a8b9c0d1_seed_shorts_podcast_presets.py -
Step 1: Create migration file
Create cofee_backend/alembic/versions/e6f7a8b9c0d1_seed_shorts_podcast_presets.py:
"""seed shorts and podcast system presets
Revision ID: e6f7a8b9c0d1
Revises: d5e6f7a8b9c0
Create Date: 2026-03-21 12:00:00.000000
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = "e6f7a8b9c0d1"
down_revision: Union[str, None] = "d5e6f7a8b9c0"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
SHORTS_PRESET = {
"id": "00000000-0000-4000-a000-000000000004",
"user_id": None,
"name": "Шортс",
"description": "Жирные субтитры для вертикальных видео — TikTok, Reels, Shorts",
"is_system": True,
"style_config": {
"text": {
"font_family": "Montserrat",
"font_size": 72,
"font_weight": 700,
"text_color": "#FFFFFF",
"highlight_color": "#FFE500",
"text_shadow": "3px 3px 0px #000000",
"text_stroke_width": 3,
"text_stroke_color": "#000000",
},
"layout": {
"vertical_position": "bottom",
"horizontal_alignment": "center",
"padding_px": 20,
"max_width_pct": 85,
"lines_per_screen": 1,
},
"animation": {
"highlight_style": "bounce",
"highlight_scale": 1.15,
"highlight_rotation_deg": 3,
"word_entrance": "pop",
"segment_transition": "zoom_in",
"fade_duration_frames": 3,
"animation_speed": 1.0,
"text_transform": "uppercase",
},
"background": {
"bg_color": "transparent",
"bg_blur_px": 0,
"bg_glow_color": None,
"bg_border_radius_px": 0,
"bg_padding_px": 0,
},
},
"preview_url": None,
"is_active": True,
}
PODCAST_PRESET = {
"id": "00000000-0000-4000-a000-000000000005",
"user_id": None,
"name": "Подкаст",
"description": "Чистые субтитры для подкастов и интервью — караоке-подсветка, фон с размытием",
"is_system": True,
"style_config": {
"text": {
"font_family": "Inter",
"font_size": 44,
"font_weight": 400,
"text_color": "#E0E0E0",
"highlight_color": "#FFFFFF",
"text_shadow": "1px 1px 3px rgba(0,0,0,0.7)",
"text_stroke_width": 0,
"text_stroke_color": "#000000",
},
"layout": {
"vertical_position": "bottom",
"horizontal_alignment": "center",
"padding_px": 20,
"max_width_pct": 90,
"lines_per_screen": 2,
},
"animation": {
"highlight_style": "karaoke",
"highlight_scale": 1.0,
"highlight_rotation_deg": 0,
"word_entrance": "none",
"segment_transition": "fade",
"fade_duration_frames": 5,
"animation_speed": 1.0,
"text_transform": "none",
},
"background": {
"bg_color": "rgba(0,0,0,0.5)",
"bg_blur_px": 8,
"bg_glow_color": None,
"bg_border_radius_px": 12,
"bg_padding_px": 16,
},
},
"preview_url": None,
"is_active": True,
}
def upgrade() -> None:
# Idempotent: check if presets with these names already exist before inserting
conn = op.get_bind()
caption_presets = sa.table(
"caption_presets",
sa.column("id", sa.UUID()),
sa.column("user_id", sa.UUID()),
sa.column("name", sa.String()),
sa.column("description", sa.Text()),
sa.column("is_system", sa.Boolean()),
sa.column("style_config", sa.JSON()),
sa.column("preview_url", sa.String()),
sa.column("is_active", sa.Boolean()),
)
for preset in [SHORTS_PRESET, PODCAST_PRESET]:
exists = conn.execute(
sa.select(sa.func.count())
.select_from(caption_presets)
.where(caption_presets.c.name == preset["name"])
.where(caption_presets.c.is_system == True) # noqa: E712
).scalar()
if not exists:
op.bulk_insert(caption_presets, [preset])
def downgrade() -> None:
conn = op.get_bind()
caption_presets = sa.table(
"caption_presets",
sa.column("id", sa.UUID()),
sa.column("is_active", sa.Boolean()),
)
for preset_id in [SHORTS_PRESET["id"], PODCAST_PRESET["id"]]:
conn.execute(
caption_presets.update()
.where(caption_presets.c.id == preset_id)
.values(is_active=False)
)
- Step 2: Apply migration
Run: cd cofee_backend && uv run alembic upgrade head
Expected: Migration applies successfully, 2 new rows in caption_presets
- Step 3: Verify presets exist via API
Run: curl -s http://localhost:8000/api/captions/presets/ -H "Authorization: Bearer <token>" | python3 -m json.tool | grep -E '"name"'
Expected: Should list "Классические", "Неон", "Минимализм", "Шортс", "Подкаст"
(If backend isn't running, verify via direct DB query instead)
- Step 4: Commit
cd cofee_backend
git add alembic/versions/e6f7a8b9c0d1_seed_shorts_podcast_presets.py
git commit -m "feat(backend): seed Шортс and Подкаст system presets"
Task 7: Extend Frontend StyleEditor
Files:
-
Modify:
cofee_frontend/src/features/project/CaptionSettingsStep/StyleEditor.tsx(lines 38-71 — FormValues type, lines 73-106 — DEFAULT_VALUES, lines 360-452 — AnimationFields) -
Step 1: Regenerate API types from updated backend schema
Run: cd cofee_frontend && bun run gen:api-types
Expected: src/shared/api/__generated__/openapi.types.ts updated with new animation fields
(Backend must be running with the schema changes applied for this to work. If not available, proceed — the local FormValues type is what the form uses.)
- Step 2: Update FormValues type
In StyleEditor.tsx, replace lines 57-63 (the animation section of FormValues):
animation: {
highlight_style:
| "color"
| "scale"
| "underline"
| "color_scale"
| "pop_in"
| "karaoke"
| "bounce"
| "glow_pulse";
highlight_scale: number;
segment_transition: "fade" | "slide" | "none" | "zoom_in" | "drop_in";
fade_duration_frames: number;
animation_speed: number;
word_entrance: "none" | "pop" | "typewriter";
highlight_rotation_deg: number;
text_transform: "none" | "uppercase" | "lowercase";
}
- Step 3: Update DEFAULT_VALUES
In StyleEditor.tsx, replace lines 92-98 (the animation section of DEFAULT_VALUES):
animation: {
highlight_style: "color" as const,
highlight_scale: 1.2,
segment_transition: "fade" as const,
fade_duration_frames: 5,
animation_speed: 1.0,
word_entrance: "none" as const,
highlight_rotation_deg: 0,
text_transform: "none" as const,
},
- Step 4: Add new options to AnimationFields
In the AnimationFields component, add the 4 new highlight style options to the existing <Select> (after line 381, the color_scale SelectItem):
<SelectItem value="pop_in">Появление</SelectItem>
<SelectItem value="karaoke">Караоке</SelectItem>
<SelectItem value="bounce">Отскок</SelectItem>
<SelectItem value="glow_pulse">Свечение</SelectItem>
Add the 2 new segment transition options to the existing <Select> (after line 414, the none SelectItem):
<SelectItem value="zoom_in">Приближение</SelectItem>
<SelectItem value="drop_in">Выпадание</SelectItem>
- Step 5: Add 3 new form fields to AnimationFields
After the last <Controller> in AnimationFields (the animation_speed slider, ending around line 451), add:
<Controller
name="animation.word_entrance"
control={control}
render={({ field }) => (
<div className={styles.fieldGroup}>
<span className={styles.fieldLabel}>Появление слов</span>
<Select
value={field.value}
onValueChange={field.onChange}
placeholder="Появление"
>
<SelectItem value="none">Все сразу</SelectItem>
<SelectItem value="pop">Выскакивание</SelectItem>
<SelectItem value="typewriter">Печатная машинка</SelectItem>
</Select>
</div>
)}
/>
<Controller
name="animation.highlight_rotation_deg"
control={control}
render={({ field }) => (
<div className={styles.sliderField}>
<Slider
label="Поворот выделения"
unit="°"
min={0}
max={15}
step={1}
value={field.value}
onChange={field.onChange}
/>
</div>
)}
/>
<Controller
name="animation.text_transform"
control={control}
render={({ field }) => (
<div className={styles.fieldGroup}>
<span className={styles.fieldLabel}>Регистр текста</span>
<Select
value={field.value}
onValueChange={field.onChange}
placeholder="Регистр"
>
<SelectItem value="none">Без изменений</SelectItem>
<SelectItem value="uppercase">ЗАГЛАВНЫЕ</SelectItem>
<SelectItem value="lowercase">строчные</SelectItem>
</Select>
</div>
)}
/>
- Step 6: Type-check frontend
Run: cd cofee_frontend && bunx tsc --noEmit
Expected: PASS (or pre-existing type errors only — see memory for known issues)
- Step 7: Commit
cd cofee_frontend
git add src/features/project/CaptionSettingsStep/StyleEditor.tsx
git commit -m "feat(frontend): add new animation options and fields to StyleEditor"
Task 8: Final Integration Verification
- Step 1: Type-check all three projects
Run these in parallel:
cd remotion_service && bunx tsc --noEmit
cd cofee_frontend && bunx tsc --noEmit
cd cofee_backend && uv run ruff check cpv3/
Expected: All pass
- Step 2: Visual verification in Remotion Studio
Run: cd remotion_service && bun run dev
In the Remotion Studio, test the two preset configs by pasting their styleConfig JSON into composition props:
- Шортс preset: Verify uppercase text, words pop in one by one, active word bounces with yellow color + rotation, zoom_in transition between segments
- Подкаст preset: Verify normal case, karaoke wipe on active word, frosted glass background, fade transition
- Step 3: Verify frontend editor shows new options
Run: cd cofee_frontend && bun dev
Open http://localhost:3000, navigate to a project → Caption Settings → create or edit a preset:
-
Verify "Анимация" tab shows all 8 highlight styles, 5 transitions
-
Verify new fields appear: "Появление слов", "Поворот выделения", "Регистр текста"
-
Verify the 2 new system presets ("Шортс", "Подкаст") appear in the preset grid
-
Step 4: End-to-end render test
If the full stack is running (backend + remotion service + S3):
- Select the "Шортс" preset
- Generate captions on a test video
- Verify the output video has uppercase text with bounce animation
- Repeat with "Подкаст" preset — verify karaoke wipe + frosted glass
- Step 5: Final commit (if any fixes were needed)
git add -A
git commit -m "fix: integration fixes for advanced remotion templates"