Document local AI director workflow
This commit is contained in:
parent
051f4e56d0
commit
e5a86257d1
|
|
@ -806,7 +806,7 @@ It writes the script only and requires an imported audio file.
|
||||||
23. [x] Add optional generated voiceover adapter interface.
|
23. [x] Add optional generated voiceover adapter interface.
|
||||||
24. [x] Add structured logs and metrics for analysis, planning, and rendering durations.
|
24. [x] Add structured logs and metrics for analysis, planning, and rendering durations.
|
||||||
25. [x] Add integration test with generated fixture clips and a short edit plan.
|
25. [x] 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.
|
26. [x] Add documentation for running a local cinematic edit with Codex or Claude as director.
|
||||||
27. [ ] Run `mvn verify` and update this checklist.
|
27. [ ] Run `mvn verify` and update this checklist.
|
||||||
|
|
||||||
## Milestone Details
|
## Milestone Details
|
||||||
|
|
@ -1358,6 +1358,18 @@ output/edit-projects/porsche-session-001/inbox/edit-plan.json
|
||||||
|
|
||||||
### Validate And Render
|
### Validate And Render
|
||||||
|
|
||||||
|
Optional imported audio assets must be placed before rendering:
|
||||||
|
|
||||||
|
```text
|
||||||
|
output/edit-projects/porsche-session-001/audio/music.wav
|
||||||
|
output/edit-projects/porsche-session-001/audio/voiceover.wav
|
||||||
|
output/edit-projects/porsche-session-001/audio/sfx/engine-rev.wav
|
||||||
|
```
|
||||||
|
|
||||||
|
SFX plan entries use the filename without `.wav` as `assetKey`. When an accepted plan contains voiceover lines,
|
||||||
|
the default `noop` provider writes `voiceover-script.txt`; record or generate that script as
|
||||||
|
`audio/voiceover.wav` before rendering if narration should be included in the final mix.
|
||||||
|
|
||||||
If `auto-render-when-plan-appears=false`, trigger rendering manually:
|
If `auto-render-when-plan-appears=false`, trigger rendering manually:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import org.example.videoclips.api.dto.CreateEditProjectRequest;
|
||||||
import org.example.videoclips.api.dto.SaveEditPlanRequest;
|
import org.example.videoclips.api.dto.SaveEditPlanRequest;
|
||||||
import org.example.videoclips.editing.EditPlanService;
|
import org.example.videoclips.editing.EditPlanService;
|
||||||
import org.example.videoclips.editing.EditProjectService;
|
import org.example.videoclips.editing.EditProjectService;
|
||||||
|
import org.example.videoclips.editing.EditRenderer;
|
||||||
import org.example.videoclips.editing.StoryboardPromptGenerator;
|
import org.example.videoclips.editing.StoryboardPromptGenerator;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
|
@ -25,13 +26,16 @@ public class EditProjectController {
|
||||||
private final EditProjectService editProjectService;
|
private final EditProjectService editProjectService;
|
||||||
private final StoryboardPromptGenerator storyboardPromptGenerator;
|
private final StoryboardPromptGenerator storyboardPromptGenerator;
|
||||||
private final EditPlanService editPlanService;
|
private final EditPlanService editPlanService;
|
||||||
|
private final EditRenderer editRenderer;
|
||||||
|
|
||||||
public EditProjectController(EditProjectService editProjectService,
|
public EditProjectController(EditProjectService editProjectService,
|
||||||
StoryboardPromptGenerator storyboardPromptGenerator,
|
StoryboardPromptGenerator storyboardPromptGenerator,
|
||||||
EditPlanService editPlanService) {
|
EditPlanService editPlanService,
|
||||||
|
EditRenderer editRenderer) {
|
||||||
this.editProjectService = editProjectService;
|
this.editProjectService = editProjectService;
|
||||||
this.storyboardPromptGenerator = storyboardPromptGenerator;
|
this.storyboardPromptGenerator = storyboardPromptGenerator;
|
||||||
this.editPlanService = editPlanService;
|
this.editPlanService = editPlanService;
|
||||||
|
this.editRenderer = editRenderer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
|
|
@ -54,4 +58,10 @@ public class EditProjectController {
|
||||||
public Object savePlan(@PathVariable String projectId, @Valid @RequestBody SaveEditPlanRequest request) {
|
public Object savePlan(@PathVariable String projectId, @Valid @RequestBody SaveEditPlanRequest request) {
|
||||||
return editPlanService.save(projectId, request);
|
return editPlanService.save(projectId, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{projectId}:render")
|
||||||
|
public Object render(@PathVariable String projectId) {
|
||||||
|
editRenderer.render(projectId);
|
||||||
|
return editProjectService.getProject(projectId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,7 @@ public class EditPlanInboxScanner {
|
||||||
private final EditProjectService projectService;
|
private final EditProjectService projectService;
|
||||||
private final EditPlanValidator validator;
|
private final EditPlanValidator validator;
|
||||||
private final Optional<EditRenderer> renderer;
|
private final Optional<EditRenderer> renderer;
|
||||||
|
private final VoiceoverGenerator voiceoverGenerator;
|
||||||
private final AtomicBoolean scanning = new AtomicBoolean();
|
private final AtomicBoolean scanning = new AtomicBoolean();
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
|
|
@ -45,9 +46,11 @@ public class EditPlanInboxScanner {
|
||||||
EditProjectStore store,
|
EditProjectStore store,
|
||||||
EditProjectService projectService,
|
EditProjectService projectService,
|
||||||
EditPlanValidator validator,
|
EditPlanValidator validator,
|
||||||
|
VoiceoverGenerator voiceoverGenerator,
|
||||||
ObjectProvider<EditRenderer> rendererProvider
|
ObjectProvider<EditRenderer> rendererProvider
|
||||||
) {
|
) {
|
||||||
this(properties, objectMapper, store, projectService, validator, rendererProvider.getIfAvailable());
|
this(properties, objectMapper, store, projectService, validator, voiceoverGenerator,
|
||||||
|
rendererProvider.getIfAvailable());
|
||||||
}
|
}
|
||||||
|
|
||||||
EditPlanInboxScanner(
|
EditPlanInboxScanner(
|
||||||
|
|
@ -57,6 +60,14 @@ public class EditPlanInboxScanner {
|
||||||
EditProjectService projectService,
|
EditProjectService projectService,
|
||||||
EditPlanValidator validator,
|
EditPlanValidator validator,
|
||||||
EditRenderer renderer
|
EditRenderer renderer
|
||||||
|
) {
|
||||||
|
this(properties, objectMapper, store, projectService, validator, null, renderer);
|
||||||
|
}
|
||||||
|
|
||||||
|
EditPlanInboxScanner(
|
||||||
|
VideoClippingProperties properties, ObjectMapper objectMapper, EditProjectStore store,
|
||||||
|
EditProjectService projectService, EditPlanValidator validator,
|
||||||
|
VoiceoverGenerator voiceoverGenerator, EditRenderer renderer
|
||||||
) {
|
) {
|
||||||
this.projectRoot = Path.of(properties.getEditing().getProjectDirectory());
|
this.projectRoot = Path.of(properties.getEditing().getProjectDirectory());
|
||||||
this.expectedPlanFileName = properties.getEditing().getLocalDirector().getExpectedPlanFileName();
|
this.expectedPlanFileName = properties.getEditing().getLocalDirector().getExpectedPlanFileName();
|
||||||
|
|
@ -66,6 +77,7 @@ public class EditPlanInboxScanner {
|
||||||
this.projectService = projectService;
|
this.projectService = projectService;
|
||||||
this.validator = validator;
|
this.validator = validator;
|
||||||
this.renderer = Optional.ofNullable(renderer);
|
this.renderer = Optional.ofNullable(renderer);
|
||||||
|
this.voiceoverGenerator = voiceoverGenerator;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Scheduled(
|
@Scheduled(
|
||||||
|
|
@ -116,6 +128,9 @@ public class EditPlanInboxScanner {
|
||||||
}
|
}
|
||||||
|
|
||||||
store.writeJson(projectId, "edit-plan.json", normalized(plan));
|
store.writeJson(projectId, "edit-plan.json", normalized(plan));
|
||||||
|
if (voiceoverGenerator != null && !plan.voiceover().isEmpty()) {
|
||||||
|
voiceoverGenerator.generateVoiceover(projectId, plan.voiceover());
|
||||||
|
}
|
||||||
move(inboxPlan, inboxPlan.resolveSibling(expectedPlanFileName + ".accepted"));
|
move(inboxPlan, inboxPlan.resolveSibling(expectedPlanFileName + ".accepted"));
|
||||||
EditProjectResponse project = projectService.getProject(projectId);
|
EditProjectResponse project = projectService.getProject(projectId);
|
||||||
projectService.updateProject(projectId, EditProjectStatus.PLANNED, Path.of(project.inputDirectory()), null);
|
projectService.updateProject(projectId, EditProjectStatus.PLANNED, Path.of(project.inputDirectory()), null);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
video-clipping:
|
||||||
|
folder-scheduler:
|
||||||
|
enabled: false
|
||||||
|
editing:
|
||||||
|
enabled: true
|
||||||
|
local-director:
|
||||||
|
enabled: true
|
||||||
|
auto-render-when-plan-appears: false
|
||||||
|
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
org.example.videoclips.editing: INFO
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package org.example.videoclips.api;
|
||||||
|
|
||||||
|
import org.example.videoclips.api.dto.EditProjectResponse;
|
||||||
|
import org.example.videoclips.editing.EditPlanService;
|
||||||
|
import org.example.videoclips.editing.EditProjectService;
|
||||||
|
import org.example.videoclips.editing.EditProjectStatus;
|
||||||
|
import org.example.videoclips.editing.EditRenderer;
|
||||||
|
import org.example.videoclips.editing.StoryboardPromptGenerator;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import java.time.Instant;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
class EditProjectRenderControllerTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void delegatesRenderAndReturnsUpdatedProject() {
|
||||||
|
EditProjectService projects = mock(EditProjectService.class);
|
||||||
|
EditRenderer renderer = mock(EditRenderer.class);
|
||||||
|
EditProjectResponse rendered = new EditProjectResponse("project", "Project", EditProjectStatus.RENDERED,
|
||||||
|
"input", "output", 60, "cinematic-porsche-promo", true, true, true,
|
||||||
|
Instant.EPOCH, Instant.EPOCH, null);
|
||||||
|
when(projects.getProject("project")).thenReturn(rendered);
|
||||||
|
EditProjectController controller = new EditProjectController(projects, mock(StoryboardPromptGenerator.class),
|
||||||
|
mock(EditPlanService.class), renderer);
|
||||||
|
|
||||||
|
Object response = controller.render("project");
|
||||||
|
|
||||||
|
verify(renderer).render("project");
|
||||||
|
assertThat(response).isEqualTo(rendered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package org.example.videoclips.editing;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.config.YamlPropertiesFactoryBean;
|
||||||
|
import org.springframework.core.io.ClassPathResource;
|
||||||
|
|
||||||
|
import java.util.Properties;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
class CinematicEditingLocalProfileTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void enablesLocalDirectorAndDisablesEightSecondFolderScheduler() {
|
||||||
|
YamlPropertiesFactoryBean factory = new YamlPropertiesFactoryBean();
|
||||||
|
factory.setResources(new ClassPathResource("application-cinematic-editing-local.yml"));
|
||||||
|
Properties properties = factory.getObject();
|
||||||
|
|
||||||
|
assertThat(properties).isNotNull();
|
||||||
|
assertThat(properties.getProperty("video-clipping.folder-scheduler.enabled")).isEqualTo("false");
|
||||||
|
assertThat(properties.getProperty("video-clipping.editing.enabled")).isEqualTo("true");
|
||||||
|
assertThat(properties.getProperty("video-clipping.editing.local-director.enabled")).isEqualTo("true");
|
||||||
|
assertThat(properties.getProperty("video-clipping.editing.local-director.auto-render-when-plan-appears"))
|
||||||
|
.isEqualTo("false");
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue