Skip to content

Commit 729a917

Browse files
committed
fix(cli): clean up an audio export's render without --out, and name the track id audioId takes
1 parent 7921b6d commit 729a917

6 files changed

Lines changed: 23 additions & 12 deletions

File tree

‎electron/cli/runnerHost.test.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ describe("createRunnerHost", () => {
8888
await writeFile(audioRenderPath(outPath), "mp4");
8989
await writeFile(path.join(dir, "demo.mp4"), "mp4");
9090
const { ipc, host } = setup();
91-
const exportOf = (format: "wav" | "mp4", out: string): CliRequest => ({
91+
const exportOf = (format: "wav" | "mp4", out: string | null): CliRequest => ({
9292
kind: "export",
9393
projectPath: path.join(dir, "p.openscreen"),
9494
outPath: out,
@@ -106,10 +106,15 @@ describe("createRunnerHost", () => {
106106
const audio = host.start(exportOf("wav", outPath));
107107
const video = host.start(exportOf("mp4", path.join(dir, "demo.mp4.render.mp4")));
108108
await writeFile(path.join(dir, "demo.mp4.render.mp4.render.mp4"), "mp4");
109+
// Without --out, a failed run reports no outputPath: the render beside p.wav still goes.
110+
const failed = host.start(exportOf("wav", null));
111+
await writeFile(audioRenderPath(path.join(dir, "p.wav")), "mp4");
109112
ipc.call(1, "cli-done", { success: true, outputPath: outPath });
110113
ipc.call(2, "cli-done", { success: true });
114+
ipc.call(3, "cli-done", { success: false, error: "render failed" });
111115
await audio.done;
112116
await video.done;
117+
await failed.done;
113118
expect((await readdir(dir)).sort()).toEqual(["demo.mp4", "demo.mp4.render.mp4.render.mp4"]);
114119
await rm(dir, { recursive: true, force: true });
115120
});

‎electron/cli/runnerHost.ts‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
type CliDoneResult,
1313
type CliProgressEvent,
1414
type CliRequest,
15+
defaultExportPath,
1516
isAudioExportFormat,
1617
} from "../../src/lib/cliContracts";
1718

@@ -109,12 +110,15 @@ export function createRunnerHost<W extends RunnerWindow>(
109110
runs.set(id, { request, handlers, resolve });
110111
});
111112
// An audio export renders an MP4 beside its output that the runner cannot delete.
113+
const audioFormat =
114+
request.kind === "export" && isAudioExportFormat(request.format) ? request.format : null;
112115
const done =
113-
request.kind === "export" && isAudioExportFormat(request.format)
116+
request.kind === "export" && audioFormat
114117
? reported.then(async (result) => {
115-
const outPath = request.outPath ?? result.outputPath;
116-
if (outPath)
117-
await rm(audioRenderPath(outPath), { force: true }).catch(() => undefined);
118+
// Derived as the runner derives it: a failed run reports no outputPath.
119+
const outPath =
120+
request.outPath ?? defaultExportPath(request.projectPath, audioFormat);
121+
await rm(audioRenderPath(outPath), { force: true }).catch(() => undefined);
118122
return result;
119123
})
120124
: reported;

‎electron/mcp/media-tools.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -917,7 +917,7 @@ export function createMediaTools(deps: MediaToolDeps): MediaTools {
917917
"exportAudio",
918918
{
919919
description:
920-
"Write a project's audio to a WAV or an M4A, exactly as exportVideo's MP4 mixes it: the recording's sound, the imported tracks with their offset, trims, gain and fades, through the same cuts and speed regions. With audioId (addAudio's, or an audioTracks id), only that track, alone, placed where it plays in the export: the file lasts as long as the video, silent around the track. It renders the project as an export does, so it takes about as long as exportVideo.",
920+
"Write a project's audio to a WAV or an M4A, exactly as exportVideo's MP4 mixes it: the recording's sound, the imported tracks with their offset, trims, gain and fades, through the same cuts and speed regions. With audioId (addAudio's, or an audioTracks entry's trackId, its id when it has no trackId), only that track, alone, placed where it plays in the export: the file lasts as long as the video, silent around the track. It renders the project as an export does, so it takes about as long as exportVideo.",
921921
inputSchema: z.object({
922922
projectId: z.string(),
923923
path: z.string().describe(AUDIO_PATH_DESCRIPTIONS[transport]),

‎src/cli/CliExportRunner.tsx‎

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {
2222
audioRenderPath,
2323
type CliDoneResult,
2424
type CliExportRequest,
25+
defaultExportPath,
2526
isAudioExportFormat,
2627
} from "@/lib/cliContracts";
2728
import { GIF_SIZE_PRESETS, type GifSizePreset } from "@/lib/exporter";
@@ -34,10 +35,6 @@ import { buildNativeClipList, buildSceneDescription } from "@/native/sceneDescri
3435
import { type ExportProject, loadDocumentProject } from "./exportProject";
3536
import { loadCliProject, probeVideoDimensions } from "./loadCliProject";
3637

37-
function replaceExtension(filePath: string, newExtension: string): string {
38-
return filePath.replace(/\.(openscreen|json)$/i, "") + newExtension;
39-
}
40-
4138
/** Mirrors ExportDialog.gifOutputDims: from the "Source" size, cap height at the preset, keep even. */
4239
function gifOutputDims(
4340
preset: GifSizePreset,
@@ -92,7 +89,7 @@ async function runExport(request: CliExportRequest): Promise<CliDoneResult> {
9289
const gifSizePreset = request.gifSizePreset ?? editor.gifSizePreset;
9390
const gifLoop = request.gifLoop ?? editor.gifLoop;
9491
const mp4FrameRate = request.mp4FrameRate;
95-
const outPath = request.outPath ?? replaceExtension(request.projectPath, `.${format}`);
92+
const outPath = request.outPath ?? defaultExportPath(request.projectPath, format);
9693
// Use the same per-clip auto-zoom suggestions as the editor, including its default span and
9794
// anchoring behavior.
9895
if (request.autoZoom) {

‎src/lib/cliContracts.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,11 @@ export function isAudioExportFormat(format: string | null): format is AudioExpor
1212
return format === "wav" || format === "m4a";
1313
}
1414

15+
/** Where an export without --out lands: beside the project, with the format's extension. */
16+
export function defaultExportPath(projectPath: string, format: string): string {
17+
return `${projectPath.replace(/\.(openscreen|json)$/i, "")}.${format}`;
18+
}
19+
1520
/** Where the audio export renders its MP4 before taking the audio out: beside the output. */
1621
export function audioRenderPath(outPath: string): string {
1722
return `${outPath}.render.mp4`;

‎website/docs/cli.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ openscreen export demo.openscreen -o voice.wav --track audio_1234 # one impo
139139
|---|---|
140140
| `-o, --out <path>` | Output file. The extension, `.mp4`, `.gif`, `.wav` or `.m4a`, sets the format. Default: the project's path with the format's extension |
141141
| `--format <mp4\|gif\|wav\|m4a>` | Override the format stored in the project. Must agree with `--out`. `wav` and `m4a` write the project's audio only, as the MP4 mixes it |
142-
| `--track <audioId>` | WAV and M4A only: write this imported audio track alone, where it plays in the MP4, with its offset, trims, gain and fades. The file lasts as long as the video. The id is the track's `audioId` (from `addAudio`, or `audioTracks` in the project) |
142+
| `--track <audioId>` | WAV and M4A only: write this imported audio track alone, where it plays in the MP4, with its offset, trims, gain and fades. The file lasts as long as the video. The id is the track's `audioId` (from `addAudio`), or an `audioTracks` entry's `trackId` in the project, its `id` when it has no `trackId` |
143143
| `--quality <medium\|good\|source>` | Output size: `medium` is 720p, `good` is 1080p, `source` follows the smallest clip after cropping, so it never upscales. MP4 only: a GIF starts from the source size, as in the app |
144144
| `--fps <24\|30\|60>` | MP4 frame rate (default 60) |
145145
| `--gif-fps <15\|20\|25\|30>` | GIF frame rate |

0 commit comments

Comments
 (0)