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.
|
||||
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.
|
||||
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.
|
||||
|
||||
## Milestone Details
|
||||
|
|
@ -1358,6 +1358,18 @@ output/edit-projects/porsche-session-001/inbox/edit-plan.json
|
|||
|
||||
### 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:
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import org.example.videoclips.api.dto.CreateEditProjectRequest;
|
|||
import org.example.videoclips.api.dto.SaveEditPlanRequest;
|
||||
import org.example.videoclips.editing.EditPlanService;
|
||||
import org.example.videoclips.editing.EditProjectService;
|
||||
import org.example.videoclips.editing.EditRenderer;
|
||||
import org.example.videoclips.editing.StoryboardPromptGenerator;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
|
@ -25,13 +26,16 @@ public class EditProjectController {
|
|||
private final EditProjectService editProjectService;
|
||||
private final StoryboardPromptGenerator storyboardPromptGenerator;
|
||||
private final EditPlanService editPlanService;
|
||||
private final EditRenderer editRenderer;
|
||||
|
||||
public EditProjectController(EditProjectService editProjectService,
|
||||
StoryboardPromptGenerator storyboardPromptGenerator,
|
||||
EditPlanService editPlanService) {
|
||||
EditPlanService editPlanService,
|
||||
EditRenderer editRenderer) {
|
||||
this.editProjectService = editProjectService;
|
||||
this.storyboardPromptGenerator = storyboardPromptGenerator;
|
||||
this.editPlanService = editPlanService;
|
||||
this.editRenderer = editRenderer;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
|
|
@ -54,4 +58,10 @@ public class EditProjectController {
|
|||
public Object savePlan(@PathVariable String projectId, @Valid @RequestBody SaveEditPlanRequest 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 EditPlanValidator validator;
|
||||
private final Optional<EditRenderer> renderer;
|
||||
private final VoiceoverGenerator voiceoverGenerator;
|
||||
private final AtomicBoolean scanning = new AtomicBoolean();
|
||||
|
||||
@Autowired
|
||||
|
|
@ -45,9 +46,11 @@ public class EditPlanInboxScanner {
|
|||
EditProjectStore store,
|
||||
EditProjectService projectService,
|
||||
EditPlanValidator validator,
|
||||
VoiceoverGenerator voiceoverGenerator,
|
||||
ObjectProvider<EditRenderer> rendererProvider
|
||||
) {
|
||||
this(properties, objectMapper, store, projectService, validator, rendererProvider.getIfAvailable());
|
||||
this(properties, objectMapper, store, projectService, validator, voiceoverGenerator,
|
||||
rendererProvider.getIfAvailable());
|
||||
}
|
||||
|
||||
EditPlanInboxScanner(
|
||||
|
|
@ -57,6 +60,14 @@ public class EditPlanInboxScanner {
|
|||
EditProjectService projectService,
|
||||
EditPlanValidator validator,
|
||||
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.expectedPlanFileName = properties.getEditing().getLocalDirector().getExpectedPlanFileName();
|
||||
|
|
@ -66,6 +77,7 @@ public class EditPlanInboxScanner {
|
|||
this.projectService = projectService;
|
||||
this.validator = validator;
|
||||
this.renderer = Optional.ofNullable(renderer);
|
||||
this.voiceoverGenerator = voiceoverGenerator;
|
||||
}
|
||||
|
||||
@Scheduled(
|
||||
|
|
@ -116,6 +128,9 @@ public class EditPlanInboxScanner {
|
|||
}
|
||||
|
||||
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"));
|
||||
EditProjectResponse project = projectService.getProject(projectId);
|
||||
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