diff --git a/docs/cinematic-video-editing-service-plan.md b/docs/cinematic-video-editing-service-plan.md index 73ba4b6..54f7cea 100644 --- a/docs/cinematic-video-editing-service-plan.md +++ b/docs/cinematic-video-editing-service-plan.md @@ -783,7 +783,7 @@ It writes the script only and requires an imported audio file. 1. [x] Add editing configuration under `video-clipping.editing`. 2. [x] Add filesystem project store for `project.json`, `analysis.json`, `edit-plan.json`, and render output. -3. [ ] Add edit project domain records and status enum. +3. [x] Add edit project domain records and status enum. 4. [ ] Add `POST /v1/edit-projects` and `GET /v1/edit-projects/{projectId}`. 5. [ ] Add clip discovery for project input directories. 6. [ ] Add `ffprobe` analysis and validation for all input clips. diff --git a/src/main/java/org/example/videoclips/editing/AudioCue.java b/src/main/java/org/example/videoclips/editing/AudioCue.java new file mode 100644 index 0000000..bf3eb8c --- /dev/null +++ b/src/main/java/org/example/videoclips/editing/AudioCue.java @@ -0,0 +1,11 @@ +package org.example.videoclips.editing; + +public record AudioCue( + String type, + String assetKey, + double timelineStartSeconds, + double timelineEndSeconds, + double gainDb, + String notes +) { +} diff --git a/src/main/java/org/example/videoclips/editing/ClipAnalysis.java b/src/main/java/org/example/videoclips/editing/ClipAnalysis.java new file mode 100644 index 0000000..06bb3d0 --- /dev/null +++ b/src/main/java/org/example/videoclips/editing/ClipAnalysis.java @@ -0,0 +1,21 @@ +package org.example.videoclips.editing; + +import java.util.List; + +public record ClipAnalysis( + String clipId, + String sourcePath, + double durationSeconds, + String videoCodec, + String audioCodec, + int width, + int height, + double frameRate, + List thumbnails, + String contactSheet, + String proxyPath, + double motionScore, + double brightnessScore, + double sharpnessScore +) { +} diff --git a/src/main/java/org/example/videoclips/editing/EditDecision.java b/src/main/java/org/example/videoclips/editing/EditDecision.java new file mode 100644 index 0000000..97dd576 --- /dev/null +++ b/src/main/java/org/example/videoclips/editing/EditDecision.java @@ -0,0 +1,15 @@ +package org.example.videoclips.editing; + +public record EditDecision( + String clipId, + double sourceStartSeconds, + double sourceEndSeconds, + double timelineStartSeconds, + double timelineEndSeconds, + String transitionIn, + String transitionOut, + double playbackSpeed, + String visualTreatment, + String reason +) { +} diff --git a/src/main/java/org/example/videoclips/editing/EditPlan.java b/src/main/java/org/example/videoclips/editing/EditPlan.java new file mode 100644 index 0000000..3c4333d --- /dev/null +++ b/src/main/java/org/example/videoclips/editing/EditPlan.java @@ -0,0 +1,15 @@ +package org.example.videoclips.editing; + +import java.util.List; + +public record EditPlan( + String projectId, + String style, + double targetDurationSeconds, + List decisions, + List audioCues, + List voiceover, + String renderProfile, + String summary +) { +} diff --git a/src/main/java/org/example/videoclips/editing/EditProject.java b/src/main/java/org/example/videoclips/editing/EditProject.java new file mode 100644 index 0000000..93dc3b7 --- /dev/null +++ b/src/main/java/org/example/videoclips/editing/EditProject.java @@ -0,0 +1,15 @@ +package org.example.videoclips.editing; + +import java.time.Instant; + +public record EditProject( + String id, + String name, + EditProjectStatus status, + String inputDirectory, + String outputDirectory, + Instant createdAt, + Instant updatedAt, + String failureReason +) { +} diff --git a/src/main/java/org/example/videoclips/editing/EditProjectStatus.java b/src/main/java/org/example/videoclips/editing/EditProjectStatus.java new file mode 100644 index 0000000..8d54504 --- /dev/null +++ b/src/main/java/org/example/videoclips/editing/EditProjectStatus.java @@ -0,0 +1,13 @@ +package org.example.videoclips.editing; + +public enum EditProjectStatus { + CREATED, + ANALYZING, + ANALYZED, + WAITING_FOR_DIRECTOR, + PLANNING, + PLANNED, + RENDERING, + RENDERED, + FAILED +} diff --git a/src/main/java/org/example/videoclips/editing/VoiceoverLine.java b/src/main/java/org/example/videoclips/editing/VoiceoverLine.java new file mode 100644 index 0000000..9ea1499 --- /dev/null +++ b/src/main/java/org/example/videoclips/editing/VoiceoverLine.java @@ -0,0 +1,9 @@ +package org.example.videoclips.editing; + +public record VoiceoverLine( + String text, + double timelineStartSeconds, + double timelineEndSeconds, + String delivery +) { +} diff --git a/src/test/java/org/example/videoclips/editing/EditDomainJsonTest.java b/src/test/java/org/example/videoclips/editing/EditDomainJsonTest.java new file mode 100644 index 0000000..c3ec0f9 --- /dev/null +++ b/src/test/java/org/example/videoclips/editing/EditDomainJsonTest.java @@ -0,0 +1,93 @@ +package org.example.videoclips.editing; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; + +import java.time.Instant; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class EditDomainJsonTest { + + private final ObjectMapper objectMapper = new ObjectMapper().findAndRegisterModules(); + + @Test + void serializesAndDeserializesEditProject() throws Exception { + EditProject project = new EditProject( + "porsche-session-001", + "Porsche Session", + EditProjectStatus.WAITING_FOR_DIRECTOR, + "./input/editing/processed/porsche-session-001", + "./output/edit-projects/porsche-session-001", + Instant.parse("2026-07-10T10:00:00Z"), + Instant.parse("2026-07-10T10:01:00Z"), + null + ); + + String json = objectMapper.writeValueAsString(project); + + assertThat(json).contains("\"inputDirectory\""); + assertThat(json).contains("\"WAITING_FOR_DIRECTOR\""); + assertThat(objectMapper.readValue(json, EditProject.class)).isEqualTo(project); + } + + @Test + void serializesAndDeserializesClipAnalysis() throws Exception { + ClipAnalysis analysis = new ClipAnalysis( + "clip_00001", + "./clips/clip_00001.mp4", + 8.0, + "h264", + "aac", + 1920, + 1080, + 30.0, + List.of("./thumbnails/clip_00001_0001.jpg"), + "./contact-sheets/clip_00001.jpg", + "./proxies/clip_00001.mp4", + 0.8, + 0.6, + 0.9 + ); + + String json = objectMapper.writeValueAsString(analysis); + + assertThat(json).contains("\"clipId\""); + assertThat(json).contains("\"durationSeconds\""); + assertThat(objectMapper.readValue(json, ClipAnalysis.class)).isEqualTo(analysis); + } + + @Test + void serializesAndDeserializesEditPlan() throws Exception { + EditPlan plan = new EditPlan( + "porsche-session-001", + "cinematic-porsche-promo", + 60.0, + List.of(new EditDecision( + "clip_00001", + 0.0, + 2.5, + 0.0, + 2.5, + "cut", + "crossfade", + 1.0, + "high contrast cinematic grade", + "hero front angle" + )), + List.of(new AudioCue("music", "cinematic-driving-bed", 0.0, 60.0, -12.0, "build energy")), + List.of(new VoiceoverLine("This Porsche turns precision into emotion.", 3.0, 6.0, + "confident cinematic narrator")), + "mp4-h264-aac-1080p", + "Cinematic Porsche promo edit." + ); + + String json = objectMapper.writeValueAsString(plan); + + assertThat(json).contains("\"targetDurationSeconds\""); + assertThat(json).contains("\"visualTreatment\""); + assertThat(json).contains("\"voiceover\""); + assertThat(objectMapper.readValue(json, EditPlan.class)).isEqualTo(plan); + } +}