Skip to main content

剪辑片段

推荐的方法是使用录制界面记录许多小的片段,并创建许多场景。
由于错误较少且有多个场景,您将不必重复拍摍。

如果这不可行,并且您已经有一个长时间的录制,想要事后拆分,您可以使用以下脚本:

cut.ts
tsx
import { $ } from "bun";
import { existsSync, readdirSync, renameSync, rmSync } from "fs";
import { join } from "path";
import {
ALTERNATIVE1_PREFIX,
ALTERNATIVE2_PREFIX,
CAPTIONS_PREFIX,
DISPLAY_PREFIX,
WEBCAM_PREFIX,
} from "./config/cameras";
const compositionId = process.argv[2];
if (!compositionId) {
throw new Error("Expected composition ID as first argument");
}
console.log("Composition ID", compositionId);
const timestamp = process.argv[3];
if (!timestamp) {
throw new Error("Expected timestamp as second argument");
}
const trimPoint = process.argv[4];
if (!trimPoint) {
throw new Error("Expected trim point as third argument");
}
console.log("Timestamp", timestamp);
console.log("Trim point", trimPoint);
const folder = join("public", compositionId);
const files = readdirSync(folder);
const webcamFile = files.find((f) => f.startsWith(`webcam${timestamp}`));
if (!webcamFile) {
throw new Error(
`Expected file ${compositionId}/webcam${timestamp}.* to exist`,
);
}
const webcamTimestamp = webcamFile.match(/\d{1,14}/g)?.[0] ?? null;
if (!webcamTimestamp) {
throw new Error(
`Expected file ${compositionId}/webcam${timestamp}.* to have timestamp ${timestamp}`,
);
}
const allFilesWithTimestamp = files.filter((f) => {
return (
f.startsWith(`${WEBCAM_PREFIX}${timestamp}`) ||
f.startsWith(`${DISPLAY_PREFIX}${timestamp}`) ||
f.startsWith(`${ALTERNATIVE1_PREFIX}${timestamp}.mp4`) ||
f.startsWith(`${ALTERNATIVE2_PREFIX}${timestamp}.mp4`)
);
});
const allTimestamps = [
...new Set(
files.map((f) => {
return f.match(/\d{1,14}/g)?.[0] ?? null;
}),
),
];
const timestampBefore = Number(
allTimestamps.filter((t) => Number(t) < Number(webcamTimestamp) ?? null) ??
"0",
);
const newTimestamp =
(Number(webcamTimestamp) - timestampBefore) / 2 + timestampBefore;
for (const file of allFilesWithTimestamp) {
const newFile = file.replace(
new RegExp(`${webcamTimestamp}`),
String(newTimestamp),
);
// Create video before trim point
await $`bunx ffmpeg -i ${file} -t ${trimPoint} -c:v copy -c:a copy ${newFile}`.cwd(
folder,
);
// Create video after trim point
const temporaryFile = `temp-${newFile}`;
await $`bunx ffmpeg -ss ${trimPoint} -accurate_seek -i ${file} ${temporaryFile}`.cwd(
folder,
);
rmSync(join(folder, file));
renameSync(join(folder, temporaryFile), join(folder, file));
}
// Remove the captions file
const captionsFile = join(folder, `${CAPTIONS_PREFIX}${timestamp}.json`);
if (existsSync(captionsFile)) {
rmSync(captionsFile);
}
cut.ts
tsx
import { $ } from "bun";
import { existsSync, readdirSync, renameSync, rmSync } from "fs";
import { join } from "path";
import {
ALTERNATIVE1_PREFIX,
ALTERNATIVE2_PREFIX,
CAPTIONS_PREFIX,
DISPLAY_PREFIX,
WEBCAM_PREFIX,
} from "./config/cameras";
const compositionId = process.argv[2];
if (!compositionId) {
throw new Error("Expected composition ID as first argument");
}
console.log("Composition ID", compositionId);
const timestamp = process.argv[3];
if (!timestamp) {
throw new Error("Expected timestamp as second argument");
}
const trimPoint = process.argv[4];
if (!trimPoint) {
throw new Error("Expected trim point as third argument");
}
console.log("Timestamp", timestamp);
console.log("Trim point", trimPoint);
const folder = join("public", compositionId);
const files = readdirSync(folder);
const webcamFile = files.find((f) => f.startsWith(`webcam${timestamp}`));
if (!webcamFile) {
throw new Error(
`Expected file ${compositionId}/webcam${timestamp}.* to exist`,
);
}
const webcamTimestamp = webcamFile.match(/\d{1,14}/g)?.[0] ?? null;
if (!webcamTimestamp) {
throw new Error(
`Expected file ${compositionId}/webcam${timestamp}.* to have timestamp ${timestamp}`,
);
}
const allFilesWithTimestamp = files.filter((f) => {
return (
f.startsWith(`${WEBCAM_PREFIX}${timestamp}`) ||
f.startsWith(`${DISPLAY_PREFIX}${timestamp}`) ||
f.startsWith(`${ALTERNATIVE1_PREFIX}${timestamp}.mp4`) ||
f.startsWith(`${ALTERNATIVE2_PREFIX}${timestamp}.mp4`)
);
});
const allTimestamps = [
...new Set(
files.map((f) => {
return f.match(/\d{1,14}/g)?.[0] ?? null;
}),
),
];
const timestampBefore = Number(
allTimestamps.filter((t) => Number(t) < Number(webcamTimestamp) ?? null) ??
"0",
);
const newTimestamp =
(Number(webcamTimestamp) - timestampBefore) / 2 + timestampBefore;
for (const file of allFilesWithTimestamp) {
const newFile = file.replace(
new RegExp(`${webcamTimestamp}`),
String(newTimestamp),
);
// Create video before trim point
await $`bunx ffmpeg -i ${file} -t ${trimPoint} -c:v copy -c:a copy ${newFile}`.cwd(
folder,
);
// Create video after trim point
const temporaryFile = `temp-${newFile}`;
await $`bunx ffmpeg -ss ${trimPoint} -accurate_seek -i ${file} ${temporaryFile}`.cwd(
folder,
);
rmSync(join(folder, file));
renameSync(join(folder, temporaryFile), join(folder, file));
}
// Remove the captions file
const captionsFile = join(folder, `${CAPTIONS_PREFIX}${timestamp}.json`);
if (existsSync(captionsFile)) {
rmSync(captionsFile);
}

以下是一个破坏性操作 - 在运行此操作之前,请提交您的更改。

要使用此脚本,请找到您想要拆分的文件,例如 my-video/webcam123456789012345.mp4
在命令行上执行以下操作:

shell
bun cut.ts my-video 123456789012345 2.0
# ^ comp ^时间戳 ^ 秒为单位的切割点
shell
bun cut.ts my-video 123456789012345 2.0
# ^ comp ^时间戳 ^ 秒为单位的切割点

这将把所有录制分成两部分。
它还会删除字幕文件 - 要重新生成它,请运行:

shell
bun sub.ts
shell
bun sub.ts

现在,在 Studio 中,在您刚刚编辑的场景后添加一个新场景

我们计划在未来为此工作流程添加一个 GUI 快捷方式。