From edee9f5aa6620aabab5bf922e029484732d73dc9 Mon Sep 17 00:00:00 2001 From: nanameru <4869nanataitai@gmail.com> Date: Thu, 3 Sep 2026 11:36:49 +1000 Subject: [PATCH] feat(export): add 4K quality option (Refs #6) --- docs/cli.md | 4 +- electron/cli/args.test.ts | 7 ++ electron/cli/args.ts | 6 +- .../ExportDialog.showInFolder.test.tsx | 24 +++++++ src/components/ai-edition/ExportDialog.tsx | 3 +- .../video-editor/projectPersistence.test.ts | 4 ++ .../video-editor/projectPersistence.ts | 4 +- src/i18n/locales/ar/settings.json | 1 + src/i18n/locales/en/settings.json | 1 + src/i18n/locales/es/settings.json | 1 + src/i18n/locales/fr/settings.json | 1 + src/i18n/locales/it/settings.json | 1 + src/i18n/locales/ja-JP/settings.json | 1 + src/i18n/locales/ko-KR/settings.json | 1 + src/i18n/locales/pt-BR/settings.json | 1 + src/i18n/locales/ru/settings.json | 1 + src/i18n/locales/tr/settings.json | 1 + src/i18n/locales/vi/settings.json | 1 + src/i18n/locales/zh-CN/settings.json | 1 + src/i18n/locales/zh-TW/settings.json | 1 + src/lib/exporter/mp4ExportSettings.test.ts | 20 ++++++ src/lib/exporter/mp4ExportSettings.ts | 11 +++- src/lib/exporter/types.ts | 2 +- src/lib/userPreferences.test.ts | 6 ++ src/lib/userPreferences.ts | 1 + test-board.yaml | 66 +++++++++++++++++++ 26 files changed, 162 insertions(+), 9 deletions(-) create mode 100644 test-board.yaml diff --git a/docs/cli.md b/docs/cli.md index 44de0db79..06edea758 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -143,7 +143,7 @@ speed regions, wallpaper/padding, annotations, cursor rendering, webcam layouts. ```bash openscreen export demo.openscreen # format/quality from the project -openscreen export demo.openscreen -o out.mp4 --quality source +openscreen export demo.openscreen -o out.mp4 --quality 4k openscreen export demo.openscreen -o out.gif --gif-fps 20 --gif-size large openscreen export demo.openscreen --json | while read line; do ...; done ``` @@ -152,7 +152,7 @@ openscreen export demo.openscreen --json | while read line; do ...; done |---|---| | `-o, --out ` | Output file; extension picks the format. Default: next to the project | | `--format ` | Override the project's stored format | -| `--quality ` | MP4 quality | +| `--quality ` | MP4 quality | | `--gif-fps <15\|20\|25\|30>`, `--gif-size ` | GIF settings | | `--auto-zoom` | Add automatic zooms from cursor telemetry before rendering — the same dwell-detection engine as the editor's magic wand. Existing zoom regions are kept; suggestions never overlap them | | `--audio ` | Mix a voiceover file into the MP4 (mp3/wav/m4a — anything Chromium can decode; AIFF is not supported) | diff --git a/electron/cli/args.test.ts b/electron/cli/args.test.ts index 2b1f5fb6d..929ab5a8c 100644 --- a/electron/cli/args.test.ts +++ b/electron/cli/args.test.ts @@ -56,6 +56,13 @@ describe("parseCliArgs", () => { }); }); + it("accepts 4k as an MP4 export quality", () => { + expect(parse(["export", "demo.openscreen", "--quality", "4k"])).toMatchObject({ + kind: "export", + quality: "4k", + }); + }); + it("rejects a --format that conflicts with the --out extension", () => { const cmd = parse(["export", "a.openscreen", "-o", "x.mp4", "--format", "gif"]); expect(cmd).toMatchObject({ kind: "error" }); diff --git a/electron/cli/args.ts b/electron/cli/args.ts index d9e33819e..42144a508 100644 --- a/electron/cli/args.ts +++ b/electron/cli/args.ts @@ -76,7 +76,7 @@ Usage: Export options: -o, --out Output file (.mp4 or .gif). Default: next to the project file --format Override the format stored in the project - --quality + --quality MP4 quality (default: from project) --gif-fps <15|20|25|30> GIF frame rate (default: from project) --gif-size @@ -203,8 +203,8 @@ function parseExport(args: string[], cwd: string): CliCommand { } case "--quality": { const [value, next] = takeValue(args, i, arg); - if (value !== "medium" && value !== "good" && value !== "source") { - throw new Error(`--quality must be medium, good or source, got "${value}"`); + if (value !== "medium" && value !== "good" && value !== "4k" && value !== "source") { + throw new Error(`--quality must be medium, good, 4k or source, got "${value}"`); } request.quality = value; i = next; diff --git a/src/components/ai-edition/ExportDialog.showInFolder.test.tsx b/src/components/ai-edition/ExportDialog.showInFolder.test.tsx index c5b45637c..7d13523d6 100644 --- a/src/components/ai-edition/ExportDialog.showInFolder.test.tsx +++ b/src/components/ai-edition/ExportDialog.showInFolder.test.tsx @@ -26,6 +26,7 @@ vi.mock("@/native/sceneDescription", () => ({ import { toast } from "sonner"; import { I18nProvider } from "@/contexts/I18nContext"; import { type AxcutDocument, axcutSchemaVersion } from "@/lib/ai-edition/schema"; +import { exportMultiNative } from "@/native"; import { ExportDialog } from "./ExportDialog"; type ElectronAPI = Window["electronAPI"]; @@ -151,4 +152,27 @@ describe("ExportDialog done panel — show in folder", () => { ); expect(toast.error).not.toHaveBeenCalled(); }); + + it("offers a 4K button and exports a landscape project at 3840 × 2160", async () => { + render( + + + , + ); + + const fourK = screen.getByRole("button", { name: /4k/i }); + expect(fourK).toHaveTextContent("3840 × 2160"); + expect(fourK).toHaveTextContent(/upscale/i); + fireEvent.click(fourK); + fireEvent.click(screen.getByRole("button", { name: /export mp4/i })); + + await waitFor(() => + expect(vi.mocked(exportMultiNative)).toHaveBeenCalledWith( + expect.any(Array), + SAVED_PATH, + expect.any(String), + expect.objectContaining({ width: 3840, height: 2160 }), + ), + ); + }); }); diff --git a/src/components/ai-edition/ExportDialog.tsx b/src/components/ai-edition/ExportDialog.tsx index 5dff50acf..14be76800 100644 --- a/src/components/ai-edition/ExportDialog.tsx +++ b/src/components/ai-edition/ExportDialog.tsx @@ -114,6 +114,7 @@ const QUALITY_OPTIONS: Array<{ }> = [ { value: "medium", labelKey: "exportQuality.low" }, { value: "good", labelKey: "exportQuality.medium" }, + { value: "4k", labelKey: "exportQuality.ultra" }, { value: "source", labelKey: "exportQuality.high" }, ]; @@ -409,7 +410,7 @@ export function ExportDialog({ open, onClose, document }: ExportDialogProps) { > {t("exportDialog.quality")} -
+
{QUALITY_OPTIONS.map((q) => (