Files
remotion_service/docs/superpowers/plans/2026-03-21-advanced-remotion-templates.md
T
2026-03-22 22:42:35 +03:00

919 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:
```typescript
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:
```typescript
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**
```bash
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 — `StyledWord` component)
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):
```typescript
import { interpolate, spring, useVideoConfig } from "remotion";
```
- [ ] **Step 2: Add `useVideoConfig` to StyledWord**
Inside `StyledWord` (line 69), add `fps` extraction right after the destructure:
```typescript
const { fps } = useVideoConfig();
```
This is needed for `spring()` calls which require fps.
- [ ] **Step 3: Implement `pop_in` highlight style**
In `StyledWord`, inside the `if (isCurrent)` block (after the existing `underline` branch at line 132), add:
```typescript
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 `karaoke` highlight style**
After the `pop_in` block, add:
```typescript
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 `bounce` highlight style**
After the `karaoke` block, add:
```typescript
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_pulse` highlight style**
After the `bounce` block, add:
```typescript
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_deg` support**
After all highlight style branches (still inside `if (isCurrent)`), add rotation support:
```typescript
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**
```bash
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 `useVideoConfig` to the Captions component**
At the top of the `Captions` component (around line 186), add fps extraction for `spring()` calls in transitions:
```typescript
const { fps: videoFps } = useVideoConfig();
```
This is needed by `drop_in` transition (Step 4) and must be declared before use.
- [ ] **Step 2: Implement `word_entrance` in 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:
```typescript
// 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_in` and `drop_in` segment transitions**
In the `Captions` component, after the existing `slide` transition block (around line 247), add both new transitions and a `scale` variable:
```typescript
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_transform` and update `segmentStyle.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):
```typescript
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:
```typescript
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_style` to each of: `pop_in`, `karaoke`, `bounce`, `glow_pulse`
- Set `segment_transition` to each of: `zoom_in`, `drop_in`
- Set `word_entrance` to `pop` and `typewriter`
- Set `text_transform` to `uppercase`
- Set `highlight_rotation_deg` to `3`
Verify each renders without errors and the visual effect matches the spec description.
- [ ] **Step 8: Commit**
```bash
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`:
```typescript
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**
```bash
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`:
```python
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**
```bash
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`:
```python
"""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**
```bash
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`):
```typescript
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`):
```typescript
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):
```tsx
<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):
```tsx
<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:
```tsx
<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**
```bash
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:
```bash
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:
1. **Шортс preset**: Verify uppercase text, words pop in one by one, active word bounces with yellow color + rotation, zoom_in transition between segments
2. **Подкаст 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):
1. Select the "Шортс" preset
2. Generate captions on a test video
3. Verify the output video has uppercase text with bounce animation
4. Repeat with "Подкаст" preset — verify karaoke wipe + frosted glass
- [ ] **Step 5: Final commit (if any fixes were needed)**
```bash
git add -A
git commit -m "fix: integration fixes for advanced remotion templates"
```