# 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 (
``` - [ ] **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 " | 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 `` (after line 414, the `none` SelectItem): ```tsx Приближение Выпадание ``` - [ ] **Step 5: Add 3 new form fields to AnimationFields** After the last `` in `AnimationFields` (the `animation_speed` slider, ending around line 451), add: ```tsx (
Появление слов
)} /> (
)} /> (
Регистр текста
)} /> ``` - [ ] **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" ```