forked from jsl/video_editing_poc
Add cinematic editing configuration
This commit is contained in:
parent
bd7b209f8e
commit
754bf35cbe
|
|
@ -62,6 +62,101 @@ Input clips
|
|||
-> service stores render output and manifest
|
||||
```
|
||||
|
||||
## Local AI Director Workflow
|
||||
|
||||
The service should support a local workflow where the video editing service runs on the user's machine and an AI coding/chat agent such as Codex or Claude acts as the director.
|
||||
|
||||
This mode should not require the AI agent to directly process video files. The service prepares a project folder containing metadata, thumbnails, contact sheets, and a strict prompt. The AI agent reviews those artifacts, writes an `edit-plan.json`, and asks the service to render the result.
|
||||
|
||||
Recommended local folder flow:
|
||||
|
||||
```text
|
||||
input/editing/source/
|
||||
porsche-drive-001.mp4
|
||||
porsche-drive-002.mp4
|
||||
|
||||
service scheduler
|
||||
-> detects a new source folder or batch
|
||||
-> creates output/edit-projects/<project-id>/
|
||||
-> analyzes clips
|
||||
-> extracts thumbnails and contact sheets
|
||||
-> writes analysis.json
|
||||
-> writes ai-director-prompt.md
|
||||
-> marks project as WAITING_FOR_DIRECTOR
|
||||
|
||||
AI director
|
||||
-> reads ai-director-prompt.md
|
||||
-> reviews contact-sheets/ and thumbnails/
|
||||
-> selects the strongest shots
|
||||
-> writes edit-plan.json
|
||||
-> optionally writes voiceover-script.txt and audio shopping list
|
||||
|
||||
service renderer
|
||||
-> validates edit-plan.json
|
||||
-> renders final.mp4
|
||||
-> writes render-manifest.json
|
||||
```
|
||||
|
||||
Suggested local command:
|
||||
|
||||
```bash
|
||||
mvn spring-boot:run -Dspring-boot.run.profiles=cinematic-editing-local
|
||||
```
|
||||
|
||||
Then place a folder of clips here:
|
||||
|
||||
```text
|
||||
input/editing/source/porsche-session-001/
|
||||
```
|
||||
|
||||
The service should produce:
|
||||
|
||||
```text
|
||||
output/edit-projects/<project-id>/ai-director-prompt.md
|
||||
output/edit-projects/<project-id>/analysis.json
|
||||
output/edit-projects/<project-id>/contact-sheets/
|
||||
output/edit-projects/<project-id>/thumbnails/
|
||||
```
|
||||
|
||||
The AI director should only need those generated artifacts to make editing decisions. It should not need to inspect full-resolution clips unless the user explicitly asks for manual review of a specific moment.
|
||||
|
||||
### AI Director Responsibilities
|
||||
|
||||
The AI director should:
|
||||
|
||||
- judge clips from thumbnails, contact sheets, metadata, and optional proxies
|
||||
- identify the best hero shots, detail shots, motion shots, and ending shot
|
||||
- choose clip order and trim ranges
|
||||
- write a strict `edit-plan.json`
|
||||
- write or update voiceover lines
|
||||
- suggest music and SFX cues
|
||||
- keep the final timeline within the configured target duration
|
||||
|
||||
The AI director should not:
|
||||
|
||||
- render video
|
||||
- run heavy FFmpeg commands unless troubleshooting
|
||||
- modify source clips
|
||||
- invent clip IDs or timestamps outside `analysis.json`
|
||||
- depend on private absolute paths in viewer-facing text
|
||||
|
||||
### Service Responsibilities In AI Director Mode
|
||||
|
||||
The service should:
|
||||
|
||||
- watch a configurable local source directory for new project folders
|
||||
- claim one project folder at a time
|
||||
- ignore folders ending in `.tmp`, `.part`, `.download`, or `.processing`
|
||||
- create deterministic project IDs from folder names with numeric suffixes on collision
|
||||
- extract enough thumbnails for visual judgment
|
||||
- create contact sheets that are easy for an AI vision model to inspect
|
||||
- write a director prompt with exact instructions and JSON schema
|
||||
- wait for `edit-plan.json`
|
||||
- validate the plan before rendering
|
||||
- render final output deterministically
|
||||
|
||||
This mode allows a strong AI model to do creative direction once, then a cheaper model or the service itself can execute the mechanical steps.
|
||||
|
||||
## Output Targets
|
||||
|
||||
Default final render:
|
||||
|
|
@ -110,6 +205,7 @@ enum EditProjectStatus {
|
|||
CREATED,
|
||||
ANALYZING,
|
||||
ANALYZED,
|
||||
WAITING_FOR_DIRECTOR,
|
||||
PLANNING,
|
||||
PLANNED,
|
||||
RENDERING,
|
||||
|
|
@ -337,6 +433,8 @@ output/edit-projects/<project-id>/
|
|||
voiceover.wav
|
||||
music.wav
|
||||
sfx/
|
||||
inbox/
|
||||
edit-plan.json
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
|
@ -361,8 +459,24 @@ video-clipping:
|
|||
audio-sample-rate: 48000
|
||||
video-bitrate: 12000k
|
||||
audio-bitrate: 192k
|
||||
local-director:
|
||||
enabled: true
|
||||
source-directory: ./input/editing/source
|
||||
working-directory: ./input/editing/working
|
||||
processed-directory: ./input/editing/processed
|
||||
rejected-directory: ./input/editing/rejected
|
||||
poll-interval-ms: 5000
|
||||
director-prompt-file-name: ai-director-prompt.md
|
||||
expected-plan-file-name: edit-plan.json
|
||||
auto-render-when-plan-appears: false
|
||||
```
|
||||
|
||||
Local director defaults:
|
||||
|
||||
- `auto-render-when-plan-appears=false` is safer for early development because the user can inspect `edit-plan.json` before rendering.
|
||||
- When set to `true`, the service should render automatically after a valid plan appears in the project `inbox/` folder.
|
||||
- The scheduler should process one project folder at a time, using the same repeat-prevention pattern as the input folder scheduler.
|
||||
|
||||
## FFmpeg Commands
|
||||
|
||||
### Probe Clip
|
||||
|
|
@ -529,6 +643,112 @@ Prompt rules:
|
|||
- End with the strongest exterior or motion shot.
|
||||
- Voiceover should praise the car without sounding exaggerated or fake.
|
||||
|
||||
## AI Director Prompt File
|
||||
|
||||
In local director mode, generate `ai-director-prompt.md` for Codex, Claude, or another AI instance.
|
||||
|
||||
The prompt must include:
|
||||
|
||||
- project ID and style
|
||||
- target duration
|
||||
- path to `analysis.json`
|
||||
- paths to contact sheets and thumbnails
|
||||
- exact output location for the plan
|
||||
- strict JSON schema for `edit-plan.json`
|
||||
- validation rules
|
||||
- instruction to judge shots from thumbnails/contact sheets
|
||||
- instruction to produce a cinematic Porsche promo edit
|
||||
|
||||
Suggested prompt content:
|
||||
|
||||
```md
|
||||
You are the director for a cinematic Porsche promo edit.
|
||||
|
||||
Use only the project artifacts in this folder:
|
||||
|
||||
- `analysis.json`
|
||||
- `contact-sheets/`
|
||||
- `thumbnails/`
|
||||
- optional `proxies/`
|
||||
|
||||
Your task:
|
||||
|
||||
1. Review the contact sheets and thumbnails.
|
||||
2. Pick the strongest shots.
|
||||
3. Create a cinematic edit plan with a strong opening, rising energy, and final hero shot.
|
||||
4. Write voiceover lines that praise the Porsche in a premium but believable tone.
|
||||
5. Add music and SFX cues.
|
||||
6. Write strict JSON to `inbox/edit-plan.json`.
|
||||
|
||||
Do not render video. Do not modify source clips. Do not invent clip IDs.
|
||||
```
|
||||
|
||||
The service should write a matching `director-readme.md` with short human instructions:
|
||||
|
||||
```text
|
||||
Open ai-director-prompt.md in Codex or Claude. Let the AI inspect the generated thumbnails and contact sheets. When it writes inbox/edit-plan.json, call the render endpoint or enable auto-render.
|
||||
```
|
||||
|
||||
## Local Director Scheduler
|
||||
|
||||
Add a scheduler similar to the input folder scheduler, but for edit project folders.
|
||||
|
||||
States:
|
||||
|
||||
```text
|
||||
source -> working -> processed
|
||||
source -> working -> rejected
|
||||
```
|
||||
|
||||
Folder example:
|
||||
|
||||
```text
|
||||
input/editing/source/porsche-session-001/
|
||||
clip_00000.mp4
|
||||
clip_00001.mp4
|
||||
```
|
||||
|
||||
Claimed folder:
|
||||
|
||||
```text
|
||||
input/editing/working/porsche-session-001/
|
||||
```
|
||||
|
||||
Project output:
|
||||
|
||||
```text
|
||||
output/edit-projects/porsche-session-001/
|
||||
```
|
||||
|
||||
Scheduler behavior:
|
||||
|
||||
1. scan `local-director.source-directory`
|
||||
2. pick the first valid project folder in deterministic order
|
||||
3. move it to `working-directory`
|
||||
4. create an edit project
|
||||
5. analyze clips
|
||||
6. generate `ai-director-prompt.md`
|
||||
7. write status `WAITING_FOR_DIRECTOR`
|
||||
8. move the source folder to `processed-directory` after project creation succeeds
|
||||
9. leave the project output folder ready for the AI director
|
||||
|
||||
Plan pickup behavior:
|
||||
|
||||
1. scan project `inbox/` folders for `edit-plan.json`
|
||||
2. validate the plan
|
||||
3. if `auto-render-when-plan-appears=true`, render automatically
|
||||
4. otherwise expose the project as `PLANNED` and wait for `POST /v1/edit-projects/{projectId}:render`
|
||||
|
||||
Tests:
|
||||
|
||||
- ignores temporary project folders
|
||||
- processes one project folder at a time
|
||||
- creates project artifacts and director prompt
|
||||
- does not reprocess a processed folder
|
||||
- rejects empty folders
|
||||
- validates plan pickup
|
||||
- respects `auto-render-when-plan-appears`
|
||||
|
||||
## Voiceover And Audio
|
||||
|
||||
First implementation can support imported audio assets only:
|
||||
|
|
@ -561,7 +781,7 @@ It writes the script only and requires an imported audio file.
|
|||
|
||||
## Milestones
|
||||
|
||||
1. [ ] Add editing configuration under `video-clipping.editing`.
|
||||
1. [x] Add editing configuration under `video-clipping.editing`.
|
||||
2. [ ] Add filesystem project store for `project.json`, `analysis.json`, `edit-plan.json`, and render output.
|
||||
3. [ ] Add edit project domain records and status enum.
|
||||
4. [ ] Add `POST /v1/edit-projects` and `GET /v1/edit-projects/{projectId}`.
|
||||
|
|
@ -572,19 +792,22 @@ It writes the script only and requires an imported audio file.
|
|||
9. [ ] Add optional proxy generation.
|
||||
10. [ ] Add `analysis.json` writing and reading.
|
||||
11. [ ] Add storyboard prompt generation from `analysis.json`.
|
||||
12. [ ] Add strict JSON edit plan schema and validation.
|
||||
13. [ ] Add `PUT /v1/edit-projects/{projectId}/plan`.
|
||||
14. [ ] Add simple segment rendering without transitions.
|
||||
15. [ ] Add concat-based final timeline rendering.
|
||||
16. [ ] Add music and voiceover audio mixing.
|
||||
17. [ ] Add render manifest with command metadata and output details.
|
||||
18. [ ] Add basic transition support: cut, crossfade, fade in, fade out.
|
||||
19. [ ] Add SFX cue support.
|
||||
20. [ ] Add optional generated voiceover adapter interface.
|
||||
21. [ ] Add structured logs and metrics for analysis, planning, and rendering durations.
|
||||
22. [ ] Add integration test with generated fixture clips and a short edit plan.
|
||||
23. [ ] Add documentation for running a local cinematic edit.
|
||||
24. [ ] Run `mvn verify` and update this checklist.
|
||||
12. [ ] Add local AI director prompt generation with thumbnail/contact-sheet instructions.
|
||||
13. [ ] Add local director source-folder scheduler.
|
||||
14. [ ] Add project `inbox/` plan pickup.
|
||||
15. [ ] Add strict JSON edit plan schema and validation.
|
||||
16. [ ] Add `PUT /v1/edit-projects/{projectId}/plan`.
|
||||
17. [ ] Add simple segment rendering without transitions.
|
||||
18. [ ] Add concat-based final timeline rendering.
|
||||
19. [ ] Add music and voiceover audio mixing.
|
||||
20. [ ] Add render manifest with command metadata and output details.
|
||||
21. [ ] Add basic transition support: cut, crossfade, fade in, fade out.
|
||||
22. [ ] Add SFX cue support.
|
||||
23. [ ] Add optional generated voiceover adapter interface.
|
||||
24. [ ] Add structured logs and metrics for analysis, planning, and rendering durations.
|
||||
25. [ ] Add integration test with generated fixture clips and a short edit plan.
|
||||
26. [ ] Add documentation for running a local cinematic edit with Codex or Claude as director.
|
||||
27. [ ] Run `mvn verify` and update this checklist.
|
||||
|
||||
## Milestone Details
|
||||
|
||||
|
|
@ -730,7 +953,90 @@ Acceptance criteria:
|
|||
- generated prompt can be pasted into an AI model
|
||||
- model output requirements are strict JSON
|
||||
|
||||
### Milestone 12-13: Edit Plan Validation
|
||||
### Milestone 12: Local AI Director Prompt
|
||||
|
||||
Files:
|
||||
|
||||
- `src/main/java/org/example/videoclips/editing/AiDirectorPromptGenerator.java`
|
||||
|
||||
Behavior:
|
||||
|
||||
- generate `ai-director-prompt.md`
|
||||
- include project style, target duration, clip metadata, contact sheet paths, thumbnail paths, and strict output schema
|
||||
- write human-readable `director-readme.md`
|
||||
- instruct the AI to write `inbox/edit-plan.json`
|
||||
|
||||
Tests:
|
||||
|
||||
- prompt contains `analysis.json`
|
||||
- prompt contains `contact-sheets/` and `thumbnails/`
|
||||
- prompt contains exact output path `inbox/edit-plan.json`
|
||||
- prompt instructs the AI not to render video
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- a user can open the prompt in Codex or Claude and get a valid edit plan without additional explanation
|
||||
|
||||
### Milestone 13: Local Director Source-Folder Scheduler
|
||||
|
||||
Files:
|
||||
|
||||
- `src/main/java/org/example/videoclips/editing/LocalDirectorScheduler.java`
|
||||
- `src/main/java/org/example/videoclips/editing/EditProjectDirectoryInitializer.java`
|
||||
|
||||
Behavior:
|
||||
|
||||
- create configured source, working, processed, rejected, and project output directories on startup
|
||||
- scan `video-clipping.editing.local-director.source-directory`
|
||||
- claim one project folder by moving it to working
|
||||
- ignore hidden and temporary folders
|
||||
- create an edit project
|
||||
- analyze the clips
|
||||
- generate `ai-director-prompt.md`
|
||||
- set project status to `WAITING_FOR_DIRECTOR`
|
||||
- move the claimed source folder to processed after successful analysis
|
||||
|
||||
Tests:
|
||||
|
||||
- creates all local director directories
|
||||
- selects project folders in deterministic order
|
||||
- ignores `.tmp`, `.part`, `.download`, and `.processing` folders
|
||||
- processes one project folder per scan
|
||||
- writes project prompt and analysis files
|
||||
- rejects empty project folders
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- dropping a folder into `input/editing/source` produces a ready-to-direct project under `output/edit-projects`
|
||||
|
||||
### Milestone 14: Plan Inbox Pickup
|
||||
|
||||
Files:
|
||||
|
||||
- `src/main/java/org/example/videoclips/editing/EditPlanInboxScanner.java`
|
||||
|
||||
Behavior:
|
||||
|
||||
- scan project `inbox/` folders for `edit-plan.json`
|
||||
- ignore partially written files with `.tmp`, `.part`, or `.download` suffix
|
||||
- validate the plan
|
||||
- save normalized plan to project root
|
||||
- mark status `PLANNED`
|
||||
- render automatically only when `auto-render-when-plan-appears=true`
|
||||
|
||||
Tests:
|
||||
|
||||
- ignores temporary plan files
|
||||
- rejects invalid plan files
|
||||
- accepts valid plan files
|
||||
- does not render when auto-render is disabled
|
||||
- invokes renderer when auto-render is enabled
|
||||
|
||||
Acceptance criteria:
|
||||
|
||||
- Codex or Claude can write `inbox/edit-plan.json` and the service can pick it up without a manual API call
|
||||
|
||||
### Milestone 15-16: Edit Plan Validation
|
||||
|
||||
Files:
|
||||
|
||||
|
|
@ -761,7 +1067,7 @@ Acceptance criteria:
|
|||
|
||||
- renderer never receives an invalid plan
|
||||
|
||||
### Milestone 14-17: Render Pipeline
|
||||
### Milestone 17-20: Render Pipeline
|
||||
|
||||
Files:
|
||||
|
||||
|
|
@ -792,7 +1098,7 @@ Acceptance criteria:
|
|||
- final MP4 exists after render
|
||||
- render manifest includes input clip IDs, output path, duration, and command summaries
|
||||
|
||||
### Milestone 18-19: Transitions And SFX
|
||||
### Milestone 21-22: Transitions And SFX
|
||||
|
||||
Start simple.
|
||||
|
||||
|
|
@ -818,7 +1124,7 @@ Acceptance criteria:
|
|||
|
||||
- basic cinematic polish is possible without needing a full timeline engine
|
||||
|
||||
### Milestone 20: Voiceover Generation Hook
|
||||
### Milestone 23: Voiceover Generation Hook
|
||||
|
||||
Files:
|
||||
|
||||
|
|
@ -840,14 +1146,17 @@ Acceptance criteria:
|
|||
|
||||
- service supports the workflow without requiring paid audio generation
|
||||
|
||||
### Milestone 21: Logs And Metrics
|
||||
### Milestone 24: Logs And Metrics
|
||||
|
||||
Logs:
|
||||
|
||||
- `event=edit_project_created`
|
||||
- `event=local_director_project_claimed`
|
||||
- `event=edit_analysis_started`
|
||||
- `event=edit_analysis_completed`
|
||||
- `event=ai_director_prompt_generated`
|
||||
- `event=storyboard_prompt_generated`
|
||||
- `event=edit_plan_inbox_detected`
|
||||
- `event=edit_plan_saved`
|
||||
- `event=edit_render_started`
|
||||
- `event=edit_render_completed`
|
||||
|
|
@ -869,7 +1178,7 @@ Acceptance criteria:
|
|||
|
||||
- an operator can see which stage failed and how long rendering took
|
||||
|
||||
### Milestone 22: Integration Test
|
||||
### Milestone 25: Integration Test
|
||||
|
||||
Create generated fixture clips with FFmpeg:
|
||||
|
||||
|
|
@ -879,12 +1188,14 @@ Create generated fixture clips with FFmpeg:
|
|||
|
||||
Test flow:
|
||||
|
||||
1. create project
|
||||
2. analyze clips
|
||||
3. save a short edit plan
|
||||
4. render final video
|
||||
5. probe final video
|
||||
6. assert duration is greater than zero
|
||||
1. place a folder of generated clips in local director source
|
||||
2. run the local director scheduler
|
||||
3. assert `ai-director-prompt.md` and `analysis.json` exist
|
||||
4. write a short valid `inbox/edit-plan.json`
|
||||
5. run the plan inbox scanner
|
||||
6. render final video
|
||||
7. probe final video
|
||||
8. assert duration is greater than zero
|
||||
|
||||
Skip test when `ffmpeg` or `ffprobe` is unavailable.
|
||||
|
||||
|
|
@ -904,6 +1215,7 @@ Follow these rules when assigning this plan to a cheaper coding model:
|
|||
- Reuse existing patterns from the folder scheduler for process execution, logging, and tests.
|
||||
- Keep the first renderer deterministic and simple.
|
||||
- Do not add external AI, TTS, or music-provider calls until the filesystem workflow works.
|
||||
- For local director mode, implement file handoff first. Do not embed Codex, Claude, or another model inside the Spring service.
|
||||
|
||||
Suggested prompt for the lower-cost coding model:
|
||||
|
||||
|
|
@ -913,6 +1225,225 @@ You are implementing `docs/cinematic-video-editing-service-plan.md`.
|
|||
Implement the next unchecked milestone only. Keep the change scoped. Use existing patterns from the folder scheduler package where possible. Add tests for the milestone. Run the relevant Maven test command. Update the milestone checkbox only when tests pass. Do not implement future milestones early.
|
||||
```
|
||||
|
||||
Suggested prompt for Codex or Claude acting as the local AI director:
|
||||
|
||||
```md
|
||||
You are the AI director for this local edit project.
|
||||
|
||||
Read `ai-director-prompt.md`, `analysis.json`, and the generated contact sheets and thumbnails. Create a cinematic Porsche promo edit plan. Write strict JSON to `inbox/edit-plan.json`. Do not render video and do not modify the source clips.
|
||||
```
|
||||
|
||||
## Local Runbook
|
||||
|
||||
Use this runbook after the local AI director milestones are implemented.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Install:
|
||||
|
||||
- Java 21
|
||||
- Maven
|
||||
- FFmpeg
|
||||
- ffprobe
|
||||
- Codex, Claude, or another AI instance that can read local project files and images
|
||||
|
||||
Verify tools:
|
||||
|
||||
```bash
|
||||
java -version
|
||||
mvn -version
|
||||
ffmpeg -version
|
||||
ffprobe -version
|
||||
```
|
||||
|
||||
### Start The Service
|
||||
|
||||
Run the service with the local cinematic editing profile:
|
||||
|
||||
```bash
|
||||
mvn spring-boot:run -Dspring-boot.run.profiles=cinematic-editing-local
|
||||
```
|
||||
|
||||
Expected startup behavior:
|
||||
|
||||
- creates `input/editing/source`
|
||||
- creates `input/editing/working`
|
||||
- creates `input/editing/processed`
|
||||
- creates `input/editing/rejected`
|
||||
- creates `output/edit-projects`
|
||||
- logs that local director mode is enabled
|
||||
|
||||
### Add Source Clips
|
||||
|
||||
Create one project folder under the local director source directory:
|
||||
|
||||
```text
|
||||
input/editing/source/porsche-session-001/
|
||||
```
|
||||
|
||||
Place the clips inside that folder:
|
||||
|
||||
```text
|
||||
input/editing/source/porsche-session-001/clip_00000.mp4
|
||||
input/editing/source/porsche-session-001/clip_00001.mp4
|
||||
input/editing/source/porsche-session-001/clip_00002.mp4
|
||||
```
|
||||
|
||||
Recommended copy flow for large files:
|
||||
|
||||
1. copy the folder as `porsche-session-001.tmp`
|
||||
2. wait until all files are fully copied
|
||||
3. rename it to `porsche-session-001`
|
||||
|
||||
The scheduler should ignore temporary folders ending in `.tmp`, `.part`, `.download`, or `.processing`.
|
||||
|
||||
### Wait For Analysis
|
||||
|
||||
The service should claim the folder and move it through:
|
||||
|
||||
```text
|
||||
input/editing/source/porsche-session-001
|
||||
input/editing/working/porsche-session-001
|
||||
input/editing/processed/porsche-session-001
|
||||
```
|
||||
|
||||
After analysis, find the project output:
|
||||
|
||||
```text
|
||||
output/edit-projects/porsche-session-001/
|
||||
```
|
||||
|
||||
Expected files:
|
||||
|
||||
```text
|
||||
output/edit-projects/porsche-session-001/project.json
|
||||
output/edit-projects/porsche-session-001/analysis.json
|
||||
output/edit-projects/porsche-session-001/ai-director-prompt.md
|
||||
output/edit-projects/porsche-session-001/director-readme.md
|
||||
output/edit-projects/porsche-session-001/contact-sheets/
|
||||
output/edit-projects/porsche-session-001/thumbnails/
|
||||
output/edit-projects/porsche-session-001/inbox/
|
||||
```
|
||||
|
||||
Expected status:
|
||||
|
||||
```text
|
||||
WAITING_FOR_DIRECTOR
|
||||
```
|
||||
|
||||
### Ask The AI Director To Edit
|
||||
|
||||
Open Codex, Claude, or the selected AI instance in the project output folder.
|
||||
|
||||
Use this prompt:
|
||||
|
||||
```md
|
||||
You are the AI director for this local edit project.
|
||||
|
||||
Read `ai-director-prompt.md`, `analysis.json`, and the generated contact sheets and thumbnails. Create a cinematic Porsche promo edit plan. Write strict JSON to `inbox/edit-plan.json`. Do not render video and do not modify the source clips.
|
||||
```
|
||||
|
||||
The AI should inspect:
|
||||
|
||||
- `analysis.json`
|
||||
- `contact-sheets/*.jpg`
|
||||
- `thumbnails/*.jpg`
|
||||
- optional `proxies/*.mp4`
|
||||
|
||||
The AI should write:
|
||||
|
||||
```text
|
||||
output/edit-projects/porsche-session-001/inbox/edit-plan.json
|
||||
```
|
||||
|
||||
### Validate And Render
|
||||
|
||||
If `auto-render-when-plan-appears=false`, trigger rendering manually:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/v1/edit-projects/porsche-session-001:render
|
||||
```
|
||||
|
||||
If `auto-render-when-plan-appears=true`, the service should render automatically after it detects and validates:
|
||||
|
||||
```text
|
||||
output/edit-projects/porsche-session-001/inbox/edit-plan.json
|
||||
```
|
||||
|
||||
Expected final output:
|
||||
|
||||
```text
|
||||
output/edit-projects/porsche-session-001/final.mp4
|
||||
output/edit-projects/porsche-session-001/render-manifest.json
|
||||
```
|
||||
|
||||
### Review Output
|
||||
|
||||
Check:
|
||||
|
||||
- final video exists
|
||||
- duration is close to target duration
|
||||
- music and voiceover are present if configured
|
||||
- no black frames at the beginning or end
|
||||
- no invalid clip references in `render-manifest.json`
|
||||
|
||||
Optional probe:
|
||||
|
||||
```bash
|
||||
ffprobe -v error \
|
||||
-show_entries format=duration \
|
||||
-show_entries stream=codec_type,codec_name,width,height,r_frame_rate \
|
||||
-of json \
|
||||
output/edit-projects/porsche-session-001/final.mp4
|
||||
```
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
No project appears in `output/edit-projects`:
|
||||
|
||||
- confirm the service is running with `cinematic-editing-local`
|
||||
- confirm the source folder is not still named `.tmp`, `.part`, `.download`, or `.processing`
|
||||
- confirm the folder contains at least one valid video file
|
||||
- check logs for `event=local_director_project_claimed`
|
||||
|
||||
Analysis fails:
|
||||
|
||||
- run `ffprobe` manually against one source clip
|
||||
- check whether the input file is still being copied
|
||||
- move bad files out of the project folder and try again
|
||||
|
||||
AI cannot decide from thumbnails:
|
||||
|
||||
- increase `thumbnail-count-per-clip`
|
||||
- enable proxies with `proxy-enabled=true`
|
||||
- regenerate analysis
|
||||
|
||||
`edit-plan.json` is rejected:
|
||||
|
||||
- confirm all `clipId` values exist in `analysis.json`
|
||||
- confirm source timestamps are inside each clip duration
|
||||
- confirm timeline timestamps do not overlap except for supported transitions
|
||||
- confirm the JSON is strict JSON with no markdown wrapper
|
||||
|
||||
Render fails:
|
||||
|
||||
- check `render-manifest.json` if it exists
|
||||
- check FFmpeg logs
|
||||
- confirm all referenced source clips are still available under the project
|
||||
- temporarily disable music, SFX, and voiceover to isolate video rendering
|
||||
|
||||
Final video has no audio:
|
||||
|
||||
- confirm the source clips have audio or imported audio files exist
|
||||
- confirm `music.wav` and `voiceover.wav` are valid WAV files if used
|
||||
- check the audio mix command in the render manifest
|
||||
|
||||
Final video is too slow to render:
|
||||
|
||||
- lower output resolution
|
||||
- disable proxies only after analysis is complete
|
||||
- keep the first renderer concat-based before adding expensive transitions
|
||||
|
||||
## Cost Strategy
|
||||
|
||||
The service should reduce expensive AI usage by producing compact artifacts.
|
||||
|
|
@ -943,6 +1474,9 @@ The target architecture should keep normal cinematic edit planning in the `30k-1
|
|||
The feature is complete when:
|
||||
|
||||
- a user can create an edit project from an existing folder of clips
|
||||
- a user can drop a folder into `input/editing/source` and get a ready-to-direct project locally
|
||||
- `ai-director-prompt.md` tells Codex or Claude exactly how to judge thumbnails and write `inbox/edit-plan.json`
|
||||
- the service can pick up an AI-generated `edit-plan.json` from the project inbox
|
||||
- the service can analyze clips and generate `analysis.json`
|
||||
- the service can generate a storyboard prompt
|
||||
- a valid edit plan can be saved
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ public class VideoClippingProperties {
|
|||
|
||||
private final FolderScheduler folderScheduler = new FolderScheduler();
|
||||
|
||||
private final Editing editing = new Editing();
|
||||
|
||||
private final Cleanup cleanup = new Cleanup();
|
||||
|
||||
private final Quotas quotas = new Quotas();
|
||||
|
|
@ -122,6 +124,10 @@ public class VideoClippingProperties {
|
|||
return folderScheduler;
|
||||
}
|
||||
|
||||
public Editing getEditing() {
|
||||
return editing;
|
||||
}
|
||||
|
||||
public Cleanup getCleanup() {
|
||||
return cleanup;
|
||||
}
|
||||
|
|
@ -475,6 +481,265 @@ public class VideoClippingProperties {
|
|||
}
|
||||
}
|
||||
|
||||
public static class Editing {
|
||||
private boolean enabled = true;
|
||||
|
||||
private String projectDirectory = "./output/edit-projects";
|
||||
|
||||
private String ffmpegBinary = "ffmpeg";
|
||||
|
||||
private String ffprobeBinary = "ffprobe";
|
||||
|
||||
@Min(1)
|
||||
private int thumbnailCountPerClip = 5;
|
||||
|
||||
@Min(1)
|
||||
private int contactSheetColumns = 5;
|
||||
|
||||
private boolean proxyEnabled = true;
|
||||
|
||||
@Min(1)
|
||||
private int proxyWidth = 640;
|
||||
|
||||
@Min(1)
|
||||
private int targetDurationSeconds = 60;
|
||||
|
||||
@Min(1)
|
||||
private int outputWidth = 1920;
|
||||
|
||||
@Min(1)
|
||||
private int outputHeight = 1080;
|
||||
|
||||
@Min(1)
|
||||
private int outputFrameRate = 30;
|
||||
|
||||
@Min(1)
|
||||
private int audioSampleRate = 48000;
|
||||
|
||||
private String videoBitrate = "12000k";
|
||||
|
||||
private String audioBitrate = "192k";
|
||||
|
||||
private final LocalDirector localDirector = new LocalDirector();
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public String getProjectDirectory() {
|
||||
return projectDirectory;
|
||||
}
|
||||
|
||||
public void setProjectDirectory(String projectDirectory) {
|
||||
this.projectDirectory = projectDirectory;
|
||||
}
|
||||
|
||||
public String getFfmpegBinary() {
|
||||
return ffmpegBinary;
|
||||
}
|
||||
|
||||
public void setFfmpegBinary(String ffmpegBinary) {
|
||||
this.ffmpegBinary = ffmpegBinary;
|
||||
}
|
||||
|
||||
public String getFfprobeBinary() {
|
||||
return ffprobeBinary;
|
||||
}
|
||||
|
||||
public void setFfprobeBinary(String ffprobeBinary) {
|
||||
this.ffprobeBinary = ffprobeBinary;
|
||||
}
|
||||
|
||||
public int getThumbnailCountPerClip() {
|
||||
return thumbnailCountPerClip;
|
||||
}
|
||||
|
||||
public void setThumbnailCountPerClip(int thumbnailCountPerClip) {
|
||||
this.thumbnailCountPerClip = thumbnailCountPerClip;
|
||||
}
|
||||
|
||||
public int getContactSheetColumns() {
|
||||
return contactSheetColumns;
|
||||
}
|
||||
|
||||
public void setContactSheetColumns(int contactSheetColumns) {
|
||||
this.contactSheetColumns = contactSheetColumns;
|
||||
}
|
||||
|
||||
public boolean isProxyEnabled() {
|
||||
return proxyEnabled;
|
||||
}
|
||||
|
||||
public void setProxyEnabled(boolean proxyEnabled) {
|
||||
this.proxyEnabled = proxyEnabled;
|
||||
}
|
||||
|
||||
public int getProxyWidth() {
|
||||
return proxyWidth;
|
||||
}
|
||||
|
||||
public void setProxyWidth(int proxyWidth) {
|
||||
this.proxyWidth = proxyWidth;
|
||||
}
|
||||
|
||||
public int getTargetDurationSeconds() {
|
||||
return targetDurationSeconds;
|
||||
}
|
||||
|
||||
public void setTargetDurationSeconds(int targetDurationSeconds) {
|
||||
this.targetDurationSeconds = targetDurationSeconds;
|
||||
}
|
||||
|
||||
public int getOutputWidth() {
|
||||
return outputWidth;
|
||||
}
|
||||
|
||||
public void setOutputWidth(int outputWidth) {
|
||||
this.outputWidth = outputWidth;
|
||||
}
|
||||
|
||||
public int getOutputHeight() {
|
||||
return outputHeight;
|
||||
}
|
||||
|
||||
public void setOutputHeight(int outputHeight) {
|
||||
this.outputHeight = outputHeight;
|
||||
}
|
||||
|
||||
public int getOutputFrameRate() {
|
||||
return outputFrameRate;
|
||||
}
|
||||
|
||||
public void setOutputFrameRate(int outputFrameRate) {
|
||||
this.outputFrameRate = outputFrameRate;
|
||||
}
|
||||
|
||||
public int getAudioSampleRate() {
|
||||
return audioSampleRate;
|
||||
}
|
||||
|
||||
public void setAudioSampleRate(int audioSampleRate) {
|
||||
this.audioSampleRate = audioSampleRate;
|
||||
}
|
||||
|
||||
public String getVideoBitrate() {
|
||||
return videoBitrate;
|
||||
}
|
||||
|
||||
public void setVideoBitrate(String videoBitrate) {
|
||||
this.videoBitrate = videoBitrate;
|
||||
}
|
||||
|
||||
public String getAudioBitrate() {
|
||||
return audioBitrate;
|
||||
}
|
||||
|
||||
public void setAudioBitrate(String audioBitrate) {
|
||||
this.audioBitrate = audioBitrate;
|
||||
}
|
||||
|
||||
public LocalDirector getLocalDirector() {
|
||||
return localDirector;
|
||||
}
|
||||
|
||||
public static class LocalDirector {
|
||||
private boolean enabled = true;
|
||||
|
||||
private String sourceDirectory = "./input/editing/source";
|
||||
|
||||
private String workingDirectory = "./input/editing/working";
|
||||
|
||||
private String processedDirectory = "./input/editing/processed";
|
||||
|
||||
private String rejectedDirectory = "./input/editing/rejected";
|
||||
|
||||
@Min(1000)
|
||||
private long pollIntervalMs = 5000;
|
||||
|
||||
private String directorPromptFileName = "ai-director-prompt.md";
|
||||
|
||||
private String expectedPlanFileName = "edit-plan.json";
|
||||
|
||||
private boolean autoRenderWhenPlanAppears = false;
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public String getSourceDirectory() {
|
||||
return sourceDirectory;
|
||||
}
|
||||
|
||||
public void setSourceDirectory(String sourceDirectory) {
|
||||
this.sourceDirectory = sourceDirectory;
|
||||
}
|
||||
|
||||
public String getWorkingDirectory() {
|
||||
return workingDirectory;
|
||||
}
|
||||
|
||||
public void setWorkingDirectory(String workingDirectory) {
|
||||
this.workingDirectory = workingDirectory;
|
||||
}
|
||||
|
||||
public String getProcessedDirectory() {
|
||||
return processedDirectory;
|
||||
}
|
||||
|
||||
public void setProcessedDirectory(String processedDirectory) {
|
||||
this.processedDirectory = processedDirectory;
|
||||
}
|
||||
|
||||
public String getRejectedDirectory() {
|
||||
return rejectedDirectory;
|
||||
}
|
||||
|
||||
public void setRejectedDirectory(String rejectedDirectory) {
|
||||
this.rejectedDirectory = rejectedDirectory;
|
||||
}
|
||||
|
||||
public long getPollIntervalMs() {
|
||||
return pollIntervalMs;
|
||||
}
|
||||
|
||||
public void setPollIntervalMs(long pollIntervalMs) {
|
||||
this.pollIntervalMs = pollIntervalMs;
|
||||
}
|
||||
|
||||
public String getDirectorPromptFileName() {
|
||||
return directorPromptFileName;
|
||||
}
|
||||
|
||||
public void setDirectorPromptFileName(String directorPromptFileName) {
|
||||
this.directorPromptFileName = directorPromptFileName;
|
||||
}
|
||||
|
||||
public String getExpectedPlanFileName() {
|
||||
return expectedPlanFileName;
|
||||
}
|
||||
|
||||
public void setExpectedPlanFileName(String expectedPlanFileName) {
|
||||
this.expectedPlanFileName = expectedPlanFileName;
|
||||
}
|
||||
|
||||
public boolean isAutoRenderWhenPlanAppears() {
|
||||
return autoRenderWhenPlanAppears;
|
||||
}
|
||||
|
||||
public void setAutoRenderWhenPlanAppears(boolean autoRenderWhenPlanAppears) {
|
||||
this.autoRenderWhenPlanAppears = autoRenderWhenPlanAppears;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class Quotas {
|
||||
@Min(0)
|
||||
private int maxActiveJobsPerTenant = 5;
|
||||
|
|
|
|||
|
|
@ -13,6 +13,32 @@ video-clipping:
|
|||
output-container: mp4
|
||||
exact-preset: veryfast
|
||||
preserve-input-quality: ${FOLDER_SCHEDULER_PRESERVE_INPUT_QUALITY:true}
|
||||
editing:
|
||||
enabled: ${VIDEO_EDITING_ENABLED:true}
|
||||
project-directory: ${VIDEO_EDITING_PROJECT_DIRECTORY:./output/edit-projects}
|
||||
ffmpeg-binary: ${VIDEO_EDITING_FFMPEG_BINARY:ffmpeg}
|
||||
ffprobe-binary: ${VIDEO_EDITING_FFPROBE_BINARY:ffprobe}
|
||||
thumbnail-count-per-clip: ${VIDEO_EDITING_THUMBNAIL_COUNT_PER_CLIP:5}
|
||||
contact-sheet-columns: ${VIDEO_EDITING_CONTACT_SHEET_COLUMNS:5}
|
||||
proxy-enabled: ${VIDEO_EDITING_PROXY_ENABLED:true}
|
||||
proxy-width: ${VIDEO_EDITING_PROXY_WIDTH:640}
|
||||
target-duration-seconds: ${VIDEO_EDITING_TARGET_DURATION_SECONDS:60}
|
||||
output-width: ${VIDEO_EDITING_OUTPUT_WIDTH:1920}
|
||||
output-height: ${VIDEO_EDITING_OUTPUT_HEIGHT:1080}
|
||||
output-frame-rate: ${VIDEO_EDITING_OUTPUT_FRAME_RATE:30}
|
||||
audio-sample-rate: ${VIDEO_EDITING_AUDIO_SAMPLE_RATE:48000}
|
||||
video-bitrate: ${VIDEO_EDITING_VIDEO_BITRATE:12000k}
|
||||
audio-bitrate: ${VIDEO_EDITING_AUDIO_BITRATE:192k}
|
||||
local-director:
|
||||
enabled: ${VIDEO_EDITING_LOCAL_DIRECTOR_ENABLED:true}
|
||||
source-directory: ${VIDEO_EDITING_LOCAL_DIRECTOR_SOURCE_DIRECTORY:./input/editing/source}
|
||||
working-directory: ${VIDEO_EDITING_LOCAL_DIRECTOR_WORKING_DIRECTORY:./input/editing/working}
|
||||
processed-directory: ${VIDEO_EDITING_LOCAL_DIRECTOR_PROCESSED_DIRECTORY:./input/editing/processed}
|
||||
rejected-directory: ${VIDEO_EDITING_LOCAL_DIRECTOR_REJECTED_DIRECTORY:./input/editing/rejected}
|
||||
poll-interval-ms: ${VIDEO_EDITING_LOCAL_DIRECTOR_POLL_INTERVAL_MS:5000}
|
||||
director-prompt-file-name: ${VIDEO_EDITING_LOCAL_DIRECTOR_PROMPT_FILE_NAME:ai-director-prompt.md}
|
||||
expected-plan-file-name: ${VIDEO_EDITING_LOCAL_DIRECTOR_EXPECTED_PLAN_FILE_NAME:edit-plan.json}
|
||||
auto-render-when-plan-appears: ${VIDEO_EDITING_LOCAL_DIRECTOR_AUTO_RENDER:false}
|
||||
|
||||
logging:
|
||||
level:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
package org.example.videoclips.config;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class VideoClippingPropertiesTest {
|
||||
|
||||
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(ConfigurationPropertiesAutoConfiguration.class))
|
||||
.withUserConfiguration(PropertiesConfiguration.class);
|
||||
|
||||
@Test
|
||||
void bindsEditingDefaults() {
|
||||
contextRunner.run(context -> {
|
||||
VideoClippingProperties properties = context.getBean(VideoClippingProperties.class);
|
||||
|
||||
assertThat(properties.getEditing().isEnabled()).isTrue();
|
||||
assertThat(properties.getEditing().getProjectDirectory()).isEqualTo("./output/edit-projects");
|
||||
assertThat(properties.getEditing().getThumbnailCountPerClip()).isEqualTo(5);
|
||||
assertThat(properties.getEditing().getContactSheetColumns()).isEqualTo(5);
|
||||
assertThat(properties.getEditing().isProxyEnabled()).isTrue();
|
||||
assertThat(properties.getEditing().getProxyWidth()).isEqualTo(640);
|
||||
assertThat(properties.getEditing().getTargetDurationSeconds()).isEqualTo(60);
|
||||
assertThat(properties.getEditing().getOutputWidth()).isEqualTo(1920);
|
||||
assertThat(properties.getEditing().getOutputHeight()).isEqualTo(1080);
|
||||
assertThat(properties.getEditing().getOutputFrameRate()).isEqualTo(30);
|
||||
assertThat(properties.getEditing().getAudioSampleRate()).isEqualTo(48000);
|
||||
assertThat(properties.getEditing().getVideoBitrate()).isEqualTo("12000k");
|
||||
assertThat(properties.getEditing().getAudioBitrate()).isEqualTo("192k");
|
||||
assertThat(properties.getEditing().getLocalDirector().isEnabled()).isTrue();
|
||||
assertThat(properties.getEditing().getLocalDirector().getSourceDirectory())
|
||||
.isEqualTo("./input/editing/source");
|
||||
assertThat(properties.getEditing().getLocalDirector().isAutoRenderWhenPlanAppears()).isFalse();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void bindsEditingOverrides() {
|
||||
contextRunner
|
||||
.withPropertyValues(
|
||||
"video-clipping.editing.enabled=false",
|
||||
"video-clipping.editing.project-directory=/tmp/edit-projects",
|
||||
"video-clipping.editing.thumbnail-count-per-clip=7",
|
||||
"video-clipping.editing.local-director.enabled=false",
|
||||
"video-clipping.editing.local-director.source-directory=/tmp/source",
|
||||
"video-clipping.editing.local-director.poll-interval-ms=9000",
|
||||
"video-clipping.editing.local-director.auto-render-when-plan-appears=true"
|
||||
)
|
||||
.run(context -> {
|
||||
VideoClippingProperties properties = context.getBean(VideoClippingProperties.class);
|
||||
|
||||
assertThat(properties.getEditing().isEnabled()).isFalse();
|
||||
assertThat(properties.getEditing().getProjectDirectory()).isEqualTo("/tmp/edit-projects");
|
||||
assertThat(properties.getEditing().getThumbnailCountPerClip()).isEqualTo(7);
|
||||
assertThat(properties.getEditing().getLocalDirector().isEnabled()).isFalse();
|
||||
assertThat(properties.getEditing().getLocalDirector().getSourceDirectory()).isEqualTo("/tmp/source");
|
||||
assertThat(properties.getEditing().getLocalDirector().getPollIntervalMs()).isEqualTo(9000);
|
||||
assertThat(properties.getEditing().getLocalDirector().isAutoRenderWhenPlanAppears()).isTrue();
|
||||
});
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableConfigurationProperties(VideoClippingProperties.class)
|
||||
static class PropertiesConfiguration {
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue