# 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"
```
