# Captions Wizard Integration — Design Spec ## Context The backend captions module (`/api/captions/*`) and caption generation task (`/api/tasks/captions-generate/`) are fully implemented but have no frontend UI. This spec covers integrating captions into the Project Wizard as 3 new steps, allowing users to select/manage caption presets, trigger rendering, and view/download the captioned video. ## Requirements - Add caption-settings, caption-processing, caption-result wizard steps (positions 9-11) - Full CRUD for caption presets (system + user presets) - Tab-switch layout: preset selection grid and full-page style editor - Static preview text in the editor that updates live with style changes - Reuse ProcessingStep for caption-processing - Result step: video player + download + re-render button - All UI text in Russian ## Wizard Step Flow ``` ... → subtitle-revision → caption-settings → caption-processing → caption-result ``` | Step Key | Label | Component | New? | |----------|-------|-----------|------| | `caption-settings` | Настройка субтитров | `CaptionSettingsStep` | Yes | | `caption-processing` | Обработка | `ProcessingStep` | Reused | | `caption-result` | Результат | `CaptionResultStep` | Yes | ### Navigation - `subtitle-revision` → `caption-settings` (change existing "Завершить проект" button to "Далее" + add `goToStep("caption-settings")` call) - `caption-settings` "Генерировать" → sets active job → auto-navigates to `caption-processing` - `caption-processing` job completes → auto-navigates to `caption-result` - `caption-result` "Перегенерировать" → loops back to `caption-settings` - `caption-result` "Завершить" → marks completed, wizard finished ## WizardContext Changes New state fields in `WizardContextValue`: ```typescript captionPresetId: string | null // Selected preset UUID captionStyleConfig: object | null // Inline style override (custom not-yet-saved config) captionedVideoPath: string | null // S3 path of rendered captioned video ``` These are persisted to `project.workspace_state.wizard` alongside existing fields. ### Auto-advance logic (WizardContext effect) 1. **Update `isJobActive` guard**: Add `currentStep === "caption-processing"` to the polling condition (alongside existing `"processing"` and `"transcription-processing"` checks) so task status polling fires during caption processing. 2. **New CAPTIONS_GENERATE case**: When `activeJobType === "CAPTIONS_GENERATE"` and task status becomes DONE → read `taskStatus.output_data.output_path` to get the captioned video S3 path (this data is NOT available in Redux notifications — it must come from the task status polling response). Store in `captionedVideoPath`, clear active job, navigate to `caption-result`. ### Where `transcription_id` comes from The `useSubmitCaptionGenerate` hook needs a `transcription_id`. This comes from `transcriptionArtifactId` in WizardContext (set during the transcription flow). The hook reads it from context and passes it in the request body. ## CaptionSettingsStep Two sub-views controlled by local state (`activeTab: "select" | "editor"`). ### Tab 1: Preset Selection ("Выбор пресета") **Data**: `api.useQuery("get", "/api/captions/presets/")` → returns system + user presets **Layout**: - Grid of preset cards (3 columns) - Each card: - Dark preview area with styled "Пример" text (CSS-styled based on `style_config`) - Preset name below preview - "Системный" badge for `is_system === true` - Edit (pencil) + Delete (trash) icon buttons — hidden for system presets - Last card: "+ Создать пресет" (dashed border, click opens editor) - Selected card: highlighted border (indigo) **Footer**: "← Назад" (to subtitle-revision) + "Генерировать →" (disabled until preset selected) **Actions**: - Click card → `captionPresetId = preset.id`, highlight - Click edit → `setActiveTab("editor")`, load preset's `style_config` into form - Click delete → confirmation dialog → `DELETE /api/captions/presets/{id}/` → invalidate query cache - Click "+ Создать" → `setActiveTab("editor")`, form with default values - Click "Генерировать" → call `useSubmitCaptionGenerate()` → on success: `setActiveJob(job_id, "CAPTIONS_GENERATE")`, `markStepCompleted("caption-settings")`, `goToStep("caption-processing")` ### Tab 2: Style Editor ("Редактор стиля") **Layout**: - **Top**: Large preview panel (dark bg) — "Пример субтитров" text styled live from form values - **Middle**: 4 sub-tabs for style config sections - **Bottom**: Form controls for the active sub-tab - **Footer**: "Отмена" (back to Tab 1) + "Сохранить пресет" (create or update) **Sub-tabs and controls**: | Sub-tab | Field | Control | |---------|-------|---------| | Текст | font_family | Select (Lobster, Inter, Roboto, Montserrat, etc. — include Lobster as it's the backend default) | | Текст | font_size | Slider (16-96px) | | Текст | font_weight | Select (400: Обычный / 700: Жирный) — numeric values, backend expects `int` | | Текст | text_color | Color picker | | Текст | highlight_color | Color picker | | Текст | text_shadow | Toggle + text input | | Текст | text_stroke_width | Number input (0-5px) | | Текст | text_stroke_color | Color picker | | Позиция | vertical_position | Select (top / center / bottom) | | Позиция | horizontal_alignment | Select (left / center / right) | | Позиция | padding_px | Number input | | Позиция | max_width_pct | Slider (20-100%) | | Позиция | lines_per_screen | Number input (1-4) | | Анимация | highlight_style | Select (color / scale / underline / color_scale) | | Анимация | highlight_scale | Slider (1.0-2.0) | | Анимация | segment_transition | Select (fade / slide / none) | | Анимация | fade_duration_frames | Number input | | Анимация | animation_speed | Slider (0.5-2.0) | | Фон | bg_color | Color picker | | Фон | bg_blur_px | Number input (0-20) | | Фон | bg_glow_color | Color picker | | Фон | bg_border_radius_px | Number input (0-24) | | Фон | bg_padding_px | Number input (0-32) | **Form management**: `react-hook-form` with nested `CaptionStyleConfig` shape. Form field paths use the nested structure matching the backend schema: `text.font_family`, `text.font_size`, `layout.vertical_position`, `animation.highlight_style`, `background.bg_color`, etc. Preview panel applies form values as inline CSS. **Save flow**: - If editing existing preset → `PATCH /api/captions/presets/{id}/` with name + style_config - If creating new → name input + `POST /api/captions/presets/` with name + style_config - On success: invalidate presets query, switch back to Tab 1, auto-select the new/updated preset ## CaptionResultStep **Data source**: `captionedVideoPath` from WizardContext → `GET /api/files/get_file/?file_path={path}` to get presigned URL **Layout**: - Full-width video player (Vidstack MediaPlayer) with the captioned video - Info bar: file name, duration - Action buttons: - "Скачать" — triggers browser download of the presigned S3 URL - "Перегенерировать" — `goToStep("caption-settings")` to re-render with different preset - "Завершить" — `markStepCompleted("caption-result")`, wizard done ## ProcessingStep Integration ProcessingStep already reads `activeJobType` and shows different labels. Add to the `JOB_TYPE_LABELS` map: ```typescript "CAPTIONS_GENERATE": "ГЕНЕРАЦИЯ СУБТИТРОВ" ``` Auto-advance logic in WizardContext needs a new case: - When `CAPTIONS_GENERATE` job is DONE → extract captioned video path from job output, store in `captionedVideoPath`, navigate to `caption-result` ## API Hooks (New Files) ### `useSubmitCaptionGenerate.ts` ```typescript // POST /api/tasks/captions-generate/ // Body: { video_s3_path, folder: "output_files", transcription_id, project_id, preset_id?, style_config? } // Returns: { job_id, status } ``` ### `useCaptionPresets.ts` ```typescript // GET /api/captions/presets/ → list of CaptionPresetRead // POST /api/captions/presets/ → CaptionPresetCreate → CaptionPresetRead // PATCH /api/captions/presets/{id}/ → CaptionPresetUpdate → CaptionPresetRead // DELETE /api/captions/presets/{id}/ → 204 ``` ## File Structure ``` src/features/project/ ├── CaptionSettingsStep/ │ ├── index.ts │ ├── CaptionSettingsStep.tsx # Main component with tab logic │ ├── PresetGrid.tsx # Tab 1: preset cards grid │ ├── StyleEditor.tsx # Tab 2: full style editor │ ├── StylePreview.tsx # Live preview panel │ ├── useCaptionPresets.ts # Query + mutations for presets │ └── useSubmitCaptionGenerate.ts # Caption generation mutation ├── CaptionResultStep/ │ ├── index.ts │ └── CaptionResultStep.tsx # Video player + download + re-render ``` ## Files to Modify | File | Change | |------|--------| | `src/shared/context/WizardContext.tsx` | Add 3 step keys, 3 state fields, auto-advance for CAPTIONS_GENERATE | | `src/widgets/ProjectWizard/ProjectWizard.tsx` | Add steps to WIZARD_STEPS array and STEP_COMPONENTS map | | `src/features/project/ProcessingStep/ProcessingStep.tsx` | Add "CAPTIONS_GENERATE" to JOB_TYPE_LABELS | | `src/features/project/SubtitleRevisionStep/SubtitleRevisionStep.tsx` | Change "Завершить проект" button to "Далее" and add `goToStep("caption-settings")` navigation (currently has no forward navigation, only `markStepCompleted`) | | `src/shared/api/__generated__/openapi.types.ts` | Regenerate via `bun run gen:api-types` | ## Prerequisites 1. Run `bun run gen:api-types` with backend running to get latest captions preset types 2. Verify backend `/api/captions/presets/` endpoint is accessible ## Error Handling - **Caption generation fails (FAILED status)**: ProcessingStep already shows failure state with danger-colored progress. User clicks "Назад" (`goBack()`) → navigates back to `caption-settings` to re-submit. - **Preset delete fails (403)**: Show error toast — system presets cannot be deleted. - **Preset save fails (validation)**: Display field-level errors from API response. - **Result video URL expired**: Re-fetch presigned URL on player error via retry. ## Verification 1. Navigate to an existing project that has completed subtitle-revision 2. After subtitle-revision, wizard should advance to "Настройка субтитров" 3. Verify system presets (Классические, Неон, Минимализм) appear in the grid 4. Create a custom preset via the style editor, verify it appears in grid 5. Edit and delete the custom preset, verify CRUD works 6. Select a preset and click "Генерировать" → verify navigation to processing step 7. Wait for job completion → verify navigation to result step 8. Verify captioned video plays in the result step 9. Click "Перегенерировать" → verify return to caption-settings 10. Click "Скачать" → verify download works