From 20114d9c663523fc55891cbc4d3037965259edcf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=B6=85=E8=B6=85=E8=B6=85=E8=B6=85=E8=B6=85=E7=BA=A7?=
=?UTF-8?q?=E5=96=9C=E6=AC=A2=E4=BD=A0=E7=9A=84=E8=BE=BE=E5=A6=AE=E5=A8=85?=
<176143450+My-Denia@users.noreply.github.com>
Date: Mon, 12 Oct 2026 22:28:27 +0800
Subject: [PATCH 1/4] feat: mute recording audio
---
src/cli/CliExportRunner.audio.test.ts | 64 +++++
src/cli/CliExportRunner.tsx | 6 +-
.../ai-edition/ExportDialog.params.test.tsx | 10 +
src/components/ai-edition/ExportDialog.tsx | 8 +-
src/components/ai-edition/RightPanes.tsx | 18 ++
.../ai-edition/VirtualPreview.audio.test.ts | 32 +++
.../VirtualPreview.playback.test.tsx | 245 +++++++++++++++++-
src/components/ai-edition/VirtualPreview.tsx | 126 ++++++++-
src/i18n/locales/ar/settings.json | 2 +
src/i18n/locales/cs/settings.json | 2 +
src/i18n/locales/de/settings.json | 2 +
src/i18n/locales/en/settings.json | 2 +
src/i18n/locales/es/settings.json | 2 +
src/i18n/locales/fr/settings.json | 2 +
src/i18n/locales/it/settings.json | 2 +
src/i18n/locales/ja-JP/settings.json | 2 +
src/i18n/locales/ko-KR/settings.json | 2 +
src/i18n/locales/pt-BR/settings.json | 2 +
src/i18n/locales/ru/settings.json | 2 +
src/i18n/locales/tr/settings.json | 2 +
src/i18n/locales/vi/settings.json | 2 +
src/i18n/locales/zh-CN/settings.json | 2 +
src/i18n/locales/zh-TW/settings.json | 2 +
.../ai-edition/store/editorSettings.test.ts | 12 +
src/lib/ai-edition/store/editorSettings.ts | 9 +
.../store/useEditorSettings.test.ts | 19 +-
src/native/sceneDescription.test.ts | 65 ++++-
src/native/sceneDescription.ts | 12 +-
28 files changed, 623 insertions(+), 33 deletions(-)
create mode 100644 src/cli/CliExportRunner.audio.test.ts
diff --git a/src/cli/CliExportRunner.audio.test.ts b/src/cli/CliExportRunner.audio.test.ts
new file mode 100644
index 0000000000..2cdbd5fbe4
--- /dev/null
+++ b/src/cli/CliExportRunner.audio.test.ts
@@ -0,0 +1,64 @@
+import { describe, expect, it } from "vitest";
+import type { AxcutDocument } from "@/lib/ai-edition/schema";
+import { axcutSchemaVersion } from "@/lib/ai-edition/schema";
+import { buildNativeClipList } from "./CliExportRunner";
+
+const document: AxcutDocument = {
+ schemaVersion: axcutSchemaVersion,
+ project: {
+ id: "project",
+ title: "Audio mute",
+ createdAt: "2026-10-12T00:00:00.000Z",
+ updatedAt: "2026-10-12T00:00:00.000Z",
+ primaryAssetId: "recording",
+ },
+ assets: [
+ {
+ id: "recording",
+ kind: "video",
+ label: "recording.mp4",
+ originalPath: "/tmp/recording.mp4",
+ cameraTrack: null,
+ durationSec: 4,
+ video: { codec: "h264", width: 1920, height: 1080, fps: 30 },
+ },
+ ],
+ transcript: null,
+ timeline: {
+ clips: [
+ {
+ id: "clip",
+ assetId: "recording",
+ sourceStartSec: 0,
+ sourceEndSec: 4,
+ timelineStartSec: 0,
+ timelineEndSec: 4,
+ wordRefs: [],
+ origin: "user",
+ reason: "",
+ },
+ ],
+ gaps: [],
+ trimRanges: [],
+ muteRanges: [],
+ speedRanges: [],
+ captionRanges: [],
+ },
+ annotations: [],
+ zoomRanges: [],
+ audioTracks: [],
+ transcripts: [],
+ legacyEditor: null,
+};
+
+describe("CLI recording audio mute", () => {
+ it("keeps clip audio by default and disables it only for a muted project", () => {
+ expect(buildNativeClipList(document)[0]?.hasAudio).toBe(true);
+ expect(
+ buildNativeClipList({
+ ...document,
+ legacyEditor: { recordingAudioMuted: true },
+ })[0]?.hasAudio,
+ ).toBe(false);
+ });
+});
diff --git a/src/cli/CliExportRunner.tsx b/src/cli/CliExportRunner.tsx
index 85b0668519..27cff7088c 100644
--- a/src/cli/CliExportRunner.tsx
+++ b/src/cli/CliExportRunner.tsx
@@ -80,7 +80,9 @@ function replaceExtension(filePath: string, newExtension: string): string {
/** Mirrors ExportDialog.buildNativeClipList: trim-narrowed visible clips mapped
* onto the native multiclip contract. Kept in lock-step with
* buildSceneDescription so export and scene agree on the clip stream. */
-function buildNativeClipList(axcutDocument: AxcutDocument): CompositorClipInput[] {
+/** @internal Exported for the focused clip-audio policy test. */
+export function buildNativeClipList(axcutDocument: AxcutDocument): CompositorClipInput[] {
+ const recordingAudioMuted = getEditorSettings(axcutDocument).recordingAudioMuted;
const assetById = new Map(axcutDocument.assets.map((asset) => [asset.id, asset]));
return resolveVisibleClips(axcutDocument).flatMap((clip) => {
const asset = assetById.get(clip.assetId);
@@ -96,7 +98,7 @@ function buildNativeClipList(axcutDocument: AxcutDocument): CompositorClipInput[
sourceStartSec: clip.sourceStartSec,
sourceEndSec,
webcamOffsetSec: camera.offsetSec,
- hasAudio: true,
+ hasAudio: !recordingAudioMuted,
},
];
});
diff --git a/src/components/ai-edition/ExportDialog.params.test.tsx b/src/components/ai-edition/ExportDialog.params.test.tsx
index d78ad091c2..4e2de3e66f 100644
--- a/src/components/ai-edition/ExportDialog.params.test.tsx
+++ b/src/components/ai-edition/ExportDialog.params.test.tsx
@@ -142,6 +142,16 @@ describe("ExportDialog MP4 params", () => {
expect(at30).toMatchObject({ fps: 30, bitrate: expected(30) });
expect(at30?.bitrate).toBeLessThan(at60?.bitrate ?? 0);
});
+
+ it.each([false, true])("sets clip hasAudio when recording mute is %s", async (muted) => {
+ renderDialog({
+ ...DOC,
+ legacyEditor: { ...DOC.legacyEditor, recordingAudioMuted: muted },
+ });
+ await exportMp4();
+ const clips = vi.mocked(exportMultiNative).mock.calls.at(-1)?.[0];
+ expect(clips?.[0]?.hasAudio).toBe(!muted);
+ });
});
describe("ExportDialog format settings", () => {
diff --git a/src/components/ai-edition/ExportDialog.tsx b/src/components/ai-edition/ExportDialog.tsx
index a336901d85..0857319b86 100644
--- a/src/components/ai-edition/ExportDialog.tsx
+++ b/src/components/ai-edition/ExportDialog.tsx
@@ -111,6 +111,7 @@ function revealExportedFile(filePath: string): void {
* each with its asset's screen file + camera file (falls back to the screen when a clip has
* no camera — the no-webcam layout is a later step) and its source trim. */
function buildNativeClipList(document: AxcutDocument): CompositorClipInput[] {
+ const recordingAudioMuted = getEditorSettings(document).recordingAudioMuted;
const assetById = new Map(document.assets.map((a) => [a.id, a]));
return resolveVisibleClips(document).flatMap((clip) => {
const asset = assetById.get(clip.assetId);
@@ -122,9 +123,8 @@ function buildNativeClipList(document: AxcutDocument): CompositorClipInput[] {
// the single canonical precedence used by every consumer (clip.probe → asset.duration
// → timeline-length guess). See `resolveClipSourceEndSec` for the full order.
const sourceEndSec = resolveClipSourceEndSec(clip, asset);
- // ponytail: `hasAudio` stays optimistic for the same reason as in
- // `buildSceneDescription` — nothing populates `asset.audio` yet, and the
- // native side degrades cleanly on a stream-less file.
+ // `asset.audio` is not populated by the probe yet, so keep the current
+ // optimistic default unless the project explicitly mutes its recording audio.
return [
{
screenPath: asset.originalPath,
@@ -132,7 +132,7 @@ function buildNativeClipList(document: AxcutDocument): CompositorClipInput[] {
sourceStartSec: clip.sourceStartSec,
sourceEndSec,
webcamOffsetSec: camera.offsetSec,
- hasAudio: true,
+ hasAudio: !recordingAudioMuted,
},
];
});
diff --git a/src/components/ai-edition/RightPanes.tsx b/src/components/ai-edition/RightPanes.tsx
index 17f3350487..7f4f55a050 100644
--- a/src/components/ai-edition/RightPanes.tsx
+++ b/src/components/ai-edition/RightPanes.tsx
@@ -3462,6 +3462,24 @@ export function AudioPane() {
onCommit={() => void commit()}
/>
+
+ {ts("audio.muteRecording")}
+ void set({ recordingAudioMuted: checked })}
+ />
+
From 326158c0f142148d072abd8cc8b2ac6ae4d52e7a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=B6=85=E8=B6=85=E8=B6=85=E8=B6=85=E8=B6=85=E7=BA=A7?=
=?UTF-8?q?=E5=96=9C=E6=AC=A2=E4=BD=A0=E7=9A=84=E8=BE=BE=E5=A6=AE=E5=A8=85?=
<176143450+My-Denia@users.noreply.github.com>
Date: Mon, 12 Oct 2026 23:11:50 +0800
Subject: [PATCH 3/4] chore: support isolated dev user data
---
vite.config.ts | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/vite.config.ts b/vite.config.ts
index cb1bffcbf4..c0d47cb044 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -29,7 +29,13 @@ export default defineConfig({
}
const env = { ...process.env };
delete env.ELECTRON_RUN_AS_NODE;
- return startup(["."], { env });
+ // A disposable profile keeps dev launches working when the host's default
+ // %APPDATA%\openscreen directory is unavailable (for example, in a sandbox).
+ const devUserDataDir = process.env.OPENSCREEN_DEV_USER_DATA_DIR?.trim();
+ const args = devUserDataDir
+ ? [`--user-data-dir=${path.resolve(devUserDataDir)}`, "."]
+ : ["."];
+ return startup(args, { env });
},
vite: {
build: {},
From 8128683f2591b487dd98c476fdc1075aeaa7ab3e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E8=B6=85=E8=B6=85=E8=B6=85=E8=B6=85=E8=B6=85=E7=BA=A7?=
=?UTF-8?q?=E5=96=9C=E6=AC=A2=E4=BD=A0=E7=9A=84=E8=BE=BE=E5=A6=AE=E5=A8=85?=
<176143450+My-Denia@users.noreply.github.com>
Date: Mon, 12 Oct 2026 23:23:21 +0800
Subject: [PATCH 4/4] Revert "chore: support isolated dev user data"
This reverts commit 326158c0f142148d072abd8cc8b2ac6ae4d52e7a.
---
vite.config.ts | 8 +-------
1 file changed, 1 insertion(+), 7 deletions(-)
diff --git a/vite.config.ts b/vite.config.ts
index c0d47cb044..cb1bffcbf4 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -29,13 +29,7 @@ export default defineConfig({
}
const env = { ...process.env };
delete env.ELECTRON_RUN_AS_NODE;
- // A disposable profile keeps dev launches working when the host's default
- // %APPDATA%\openscreen directory is unavailable (for example, in a sandbox).
- const devUserDataDir = process.env.OPENSCREEN_DEV_USER_DATA_DIR?.trim();
- const args = devUserDataDir
- ? [`--user-data-dir=${path.resolve(devUserDataDir)}`, "."]
- : ["."];
- return startup(args, { env });
+ return startup(["."], { env });
},
vite: {
build: {},