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: {},