forked from jsl/video_editing_poc
Add edit render manifest
This commit is contained in:
parent
a3ca4826f5
commit
c3c6c7a790
|
|
@ -800,7 +800,7 @@ It writes the script only and requires an imported audio file.
|
|||
17. [x] Add simple segment rendering without transitions.
|
||||
18. [x] Add concat-based final timeline rendering.
|
||||
19. [x] Add music and voiceover audio mixing.
|
||||
20. [ ] Add render manifest with command metadata and output details.
|
||||
20. [x] 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.
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import java.nio.file.StandardCopyOption;
|
|||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.time.Instant;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
|
@ -52,27 +53,33 @@ public class FfmpegEditRenderer implements EditRenderer {
|
|||
createDirectory(work);
|
||||
Map<String, ClipAnalysis> clips = analysis.clips().stream()
|
||||
.collect(Collectors.toMap(ClipAnalysis::clipId, Function.identity()));
|
||||
List<List<String>> commands = new ArrayList<>();
|
||||
List<Path> segments = new ArrayList<>();
|
||||
for (int index = 0; index < plan.decisions().size(); index++) {
|
||||
EditDecision decision = plan.decisions().get(index);
|
||||
Path output = work.resolve("segment_%04d.mp4".formatted(index + 1));
|
||||
run(segmentCommand(clips.get(decision.clipId()).sourcePath(), decision, output));
|
||||
runAndRecord(segmentCommand(clips.get(decision.clipId()).sourcePath(), decision, output), commands);
|
||||
segments.add(output);
|
||||
}
|
||||
Path concatFile = work.resolve("concat.txt");
|
||||
writeConcatFile(concatFile, segments);
|
||||
Path timeline = work.resolve("timeline-video.mp4");
|
||||
run(concatCommand(concatFile, timeline));
|
||||
runAndRecord(concatCommand(concatFile, timeline), commands);
|
||||
Path output = store.projectDirectory(projectId).resolve("final.mp4");
|
||||
Path audioDirectory = store.projectDirectory(projectId).resolve("audio");
|
||||
Path music = audioDirectory.resolve("music.wav");
|
||||
Path voiceover = audioDirectory.resolve("voiceover.wav");
|
||||
if (Files.isRegularFile(music) || Files.isRegularFile(voiceover)) {
|
||||
run(audioMixCommand(timeline, Files.isRegularFile(music) ? music : null,
|
||||
Files.isRegularFile(voiceover) ? voiceover : null, output));
|
||||
runAndRecord(audioMixCommand(timeline, Files.isRegularFile(music) ? music : null,
|
||||
Files.isRegularFile(voiceover) ? voiceover : null, output), commands);
|
||||
} else {
|
||||
copy(timeline, output);
|
||||
}
|
||||
double duration = plan.decisions().getLast().timelineEndSeconds();
|
||||
RenderManifest manifest = new RenderManifest(projectId,
|
||||
plan.decisions().stream().map(EditDecision::clipId).toList(), output.toString(), duration,
|
||||
List.copyOf(commands), Instant.now());
|
||||
store.writeJson(projectId, "render-manifest.json", manifest);
|
||||
projectService.updateProject(projectId, EditProjectStatus.RENDERED, Path.of(project.inputDirectory()), null);
|
||||
}
|
||||
|
||||
|
|
@ -165,6 +172,11 @@ public class FfmpegEditRenderer implements EditRenderer {
|
|||
}
|
||||
}
|
||||
|
||||
private void runAndRecord(List<String> command, List<List<String>> commands) {
|
||||
run(command);
|
||||
commands.add(command);
|
||||
}
|
||||
|
||||
private static ProcessResult execute(List<String> command) throws IOException, InterruptedException {
|
||||
Process process = new ProcessBuilder(command).redirectErrorStream(true).start();
|
||||
String output = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
|
||||
|
|
|
|||
|
|
@ -0,0 +1,14 @@
|
|||
package org.example.videoclips.editing;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
|
||||
public record RenderManifest(
|
||||
String projectId,
|
||||
List<String> clipIds,
|
||||
String outputPath,
|
||||
double durationSeconds,
|
||||
List<List<String>> commands,
|
||||
Instant completedAt
|
||||
) {
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
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 RenderManifestTest {
|
||||
|
||||
@Test
|
||||
void roundTripsRenderProvenanceAsJson() throws Exception {
|
||||
ObjectMapper mapper = new ObjectMapper().findAndRegisterModules();
|
||||
RenderManifest manifest = new RenderManifest("project", List.of("clip-1", "clip-2"), "final.mp4", 8.0,
|
||||
List.of(List.of("ffmpeg", "-i", "clip.mp4", "final.mp4")),
|
||||
Instant.parse("2026-07-10T10:00:00Z"));
|
||||
|
||||
String json = mapper.writeValueAsString(manifest);
|
||||
|
||||
assertThat(json).contains("\"clipIds\"").contains("\"commands\"").contains("final.mp4");
|
||||
assertThat(mapper.readValue(json, RenderManifest.class)).isEqualTo(manifest);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue