Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions electron/ai-edition/document-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ describe("DocumentService", () => {
});

describe("createProject", () => {
it("creates a v7 doc with the given title and writes it to disk", async () => {
it("creates a v8 doc with the given title and writes it to disk", async () => {
const doc = await service.createProject("Demo Project");
expect(doc.schemaVersion).toBe(7);
expect(doc.schemaVersion).toBe(8);
expect(doc.project.title).toBe("Demo Project");
expect(doc.project.id).toMatch(/^proj_/);
expect(doc.assets).toEqual([]);
Expand Down
28 changes: 24 additions & 4 deletions src/cli/CliExportRunner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,29 @@ async function runExport(request: CliExportRequest): Promise<CliDoneResult> {
if (probed.durationMs > 0) {
axcutDocument = applyProbedDuration(axcutDocument, primaryAssetId, probed.durationMs / 1000);
}
// The screen's dimensions, for the same reason as the camera's below: the pure migration
// cannot know them, and the scene's screen box, the output frame and Auto all read them off
// `asset.video`. Without them the CLI laid every recording out as 1920x1080 while sizing the
// export from the probe, two answers to one question.
if (probed.width > 0 && probed.height > 0) {
axcutDocument = {
...axcutDocument,
assets: axcutDocument.assets.map((asset) =>
asset.id === primaryAssetId
? {
...asset,
video: {
codec: "unknown",
fps: 0,
...asset.video,
width: probed.width,
height: probed.height,
},
}
: asset,
),
};
}

// The camera's dimensions decide the PiP's layout box, and this is the one caller the
// document cannot answer for: there is no editor session here to have probed and saved
Expand Down Expand Up @@ -247,11 +270,8 @@ async function runExport(request: CliExportRequest): Promise<CliDoneResult> {

// Output sizing mirrors the ExportDialog: crop-aware smallest clip on the
// timeline, normalized to the document's aspect ratio.
const probedAssetDims: Record<string, Dims> = {
[primaryAssetId]: { width: probed.width, height: probed.height },
};
const smallestSource =
pickExtremeDims(collectEffectiveClipDims(axcutDocument, probedAssetDims), "smallest") ??
pickExtremeDims(collectEffectiveClipDims(axcutDocument), "smallest") ??
({ width: probed.width, height: probed.height } as Dims);
const aspectRatioValue = resolveAspectRatioValue(
axcutDocument,
Expand Down
4 changes: 3 additions & 1 deletion src/cli/CliRecordRunner.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
} from "@/components/video-editor/projectPersistence";
import { useScreenRecorder } from "@/hooks/useScreenRecorder";
import type { CliRecordRequest } from "@/lib/cliContracts";
import { DEFAULT_PROJECT_APPEARANCE } from "@/lib/projectDefaults";

type Phase = "init" | "recording" | "stopping" | "done";

Expand Down Expand Up @@ -91,7 +92,8 @@ function buildDefaultProject(session: {
...(session.webcamVideoPath ? { webcamVideoPath: session.webcamVideoPath } : {}),
...(session.cursorCaptureMode ? { cursorCaptureMode: session.cursorCaptureMode } : {}),
},
editor: normalizeProjectEditor({}),
// A new project: it takes the current default format rather than the v2 reading of none.
editor: normalizeProjectEditor({ aspectRatio: DEFAULT_PROJECT_APPEARANCE.aspectRatio }),
};
}

Expand Down
2 changes: 1 addition & 1 deletion src/components/ai-edition/CaptionsPane.gating.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ vi.mock("sonner", () => ({ toast: { success: vi.fn(), error: vi.fn() } }));

function documentWith(asset: AxcutAsset): AxcutDocument {
return {
schemaVersion: 7,
schemaVersion: 8,
project: {
id: "proj_1",
title: "Test",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const ASSET: AxcutAsset = {

function documentWith(captions: Record<string, unknown>): AxcutDocument {
return {
schemaVersion: 7,
schemaVersion: 8,
project: {
id: "proj_1",
title: "Test",
Expand Down
2 changes: 1 addition & 1 deletion src/components/ai-edition/EditorEmptyState.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ const sampleDoc = vi.hoisted(
// imports are initialised, so reading the constant here throws
// "Cannot access ... before initialization". The typecheck gate catches it
// if it ever falls behind the schema.
schemaVersion: 7,
schemaVersion: 8,
project: {
id: "proj_test",
title: "Test",
Expand Down
21 changes: 10 additions & 11 deletions src/components/ai-edition/PreviewCanvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,14 @@ import { locateVirtualPosition } from "@/lib/ai-edition/timeline/virtual-preview
import {
computeCameraFullscreenRect,
computeCompositeLayout,
paddedContentSize,
resolveWebcamLayoutPreset,
type WebcamCompositeLayout,
} from "@/lib/compositeLayout";
import { classifyWallpaper, resolveImageWallpaperUrl } from "@/lib/wallpaper";
import { getCssClipPath } from "@/lib/webcamMaskShapes";
import { computeCameraFullscreenProgress } from "@/lib/zoomMath/cameraFullscreenUtils";
import { clamp, clamp01 } from "@/utils/math";
import { clamp01 } from "@/utils/math";
import { AnnotationLayer } from "./AnnotationLayer";
import { NativeCompositorOverlay } from "./NativeCompositorOverlay";
import styles from "./NewEditorShell.module.css";
Expand Down Expand Up @@ -245,16 +246,13 @@ export function PreviewCanvas(props: PreviewCanvasProps) {
activeClipHasCamera,
);
const mask = settings.webcamMaskShape as WebcamMaskShape;
// ponytail: padding shrinks the available content area for ALL layouts
// (PiP/dual/stack) so the screen doesn't fill the canvas edge-to-edge.
// In vertical-stack this caps the camera strip height: at padding=0 the
// camera reaches 40% of canvas; at padding=50 it falls to ~32%;
// at padding=100 the screen takes even more.
const paddingFit = clamp(1 - (clamp(settings.padding, 0, 100) / 100) * 0.4, 0.4, 1);
const maxContentSize = {
width: Math.round(frameSize.width * paddingFit),
height: Math.round(frameSize.height * paddingFit),
};
// Padding shrinks the available content area for ALL layouts (PiP/dual/stack),
// through the same `paddedContentSize` the scene description uses.
const maxContentSize = paddedContentSize(
frameSize,
settings.padding,
settings.aspectRatio === "auto",
);
// The screen box must be fit to the CROPPED aspect ratio, not the full
// source frame's — otherwise VirtualPreview's crop math (which assumes
// its container is already correctly proportioned for the crop) would
Expand Down Expand Up @@ -288,6 +286,7 @@ export function PreviewCanvas(props: PreviewCanvasProps) {
settings.webcamSizePreset,
settings.webcamPosition,
settings.padding,
settings.aspectRatio,
]);

// Full Camera: during a cameraFullscreen region the webcam takes the whole
Expand Down
32 changes: 28 additions & 4 deletions src/components/ai-edition/RightPanes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ import { WALLPAPER_MOTIONS, type WallpaperMotion } from "@/components/video-edit
import { useI18n, useScopedT } from "@/contexts/I18nContext";
import { resolveCaptionLane } from "@/lib/ai-edition/captions/settings";
import { collapseTracksToPills, trackGroupId } from "@/lib/ai-edition/document/audioTracks";
import { collectNativeFormats } from "@/lib/ai-edition/document/outputFormat";
import { collectNativeFormats, pickOutputDims } from "@/lib/ai-edition/document/outputFormat";
import type { InsertSide } from "@/lib/ai-edition/document/transcript";
import type {
AxcutAsset,
Expand Down Expand Up @@ -2420,6 +2420,9 @@ export function VideoEffectsPane() {
// can never disagree about what shape the footage is. Already sorted by clip count then by
// pixel area, so [0] is "the shape most of this timeline is in" with no heuristic of ours.
const nativeFormats = useMemo(() => (document ? collectNativeFormats(document) : []), [document]);
// What Auto resolves to right now, shown on its row: the one entry whose shape moves with
// the project (padding, camera layout, crop) has to say where it currently stands.
const autoDims = useMemo(() => (document ? pickOutputDims(document, "auto") : null), [document]);
const hasTiltedZoom = (document?.zoomRanges ?? []).some((z) => z.rotationPreset != null);
const [fitMenuOpen, setFitMenuOpen] = useState(false);
const [ratioMenuOpen, setRatioMenuOpen] = useState(false);
Expand Down Expand Up @@ -2545,9 +2548,11 @@ export function VideoEffectsPane() {
bakes it into a concrete token once clip dimensions are known, and
leaves it alone until then. The group header below is localized, so
without this the two would disagree in twelve locales. */}
{settings.aspectRatio === "native"
? ts("effects.formatOriginal")
: getAspectRatioLabel(settings.aspectRatio)}
{settings.aspectRatio === "auto"
? ts("effects.formatAuto")
: settings.aspectRatio === "native"
? ts("effects.formatOriginal")
: getAspectRatioLabel(settings.aspectRatio)}
<ChevronDown size={11} />
</button>
</PopoverTrigger>
Expand All @@ -2559,6 +2564,25 @@ export function VideoEffectsPane() {
className="w-auto border-0 bg-transparent p-0 shadow-none"
>
<div className={styles.actionMenu} role="menu" aria-label={ts("effects.format")}>
{/* Auto leads: it is the one entry that is a rule rather than a shape. */}
<button
type="button"
role="menuitem"
className={`${styles.actionMenuRow}${
settings.aspectRatio === "auto" ? ` ${styles.isActive}` : ""
}`}
onClick={() => {
setRatioMenuOpen(false);
void set({ aspectRatio: "auto" });
}}
>
<span className={styles.actionMenuMain}>{ts("effects.formatAuto")}</span>
{autoDims ? (
<span className={styles.actionMenuCount}>
{`${autoDims.width}×${autoDims.height}`}
</span>
) : null}
</button>
{ASPECT_RATIO_PRESETS.map((ratio) => (
<button
type="button"
Expand Down
2 changes: 1 addition & 1 deletion src/components/ai-edition/TranscriptPane.lanes.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ function mount(audioTracks: AxcutAudioTrack[]) {
useProjectStore.setState({
projectId: "proj_1",
document: {
schemaVersion: 7,
schemaVersion: 8,
project: {
id: "proj_1",
title: "T",
Expand Down
3 changes: 3 additions & 0 deletions src/components/video-editor/editorDefaults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,9 @@ export const DEFAULT_EDITOR_LAYOUT_SETTINGS: {
wallpaper: string;
} = {
padding: 50,
// What a v2 project file means when it states no ratio. Every such file predates Auto, so
// the answer stays 16:9 for good, as the v8 upgrader pins it for documents. New projects
// state their ratio instead (see CliRecordRunner).
aspectRatio: "16:9",
cropRegion: DEFAULT_CROP_REGION,
wallpaper: DEFAULT_WALLPAPER,
Expand Down
6 changes: 4 additions & 2 deletions src/components/video-editor/projectPersistence.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,16 +126,18 @@ function computeNormalizedWebcamLayoutPreset(
webcamLayoutPreset: Partial<ProjectEditorState>["webcamLayoutPreset"],
normalizedAspectRatio: AspectRatio,
): WebcamLayoutPreset {
// Auto takes its shape FROM the layout, so there is no orientation to gate the blocks on.
const auto = normalizedAspectRatio === "auto";
switch (webcamLayoutPreset) {
case "picture-in-picture":
case "no-webcam":
return webcamLayoutPreset;
case "vertical-stack":
return isPortraitAspectRatio(normalizedAspectRatio)
return auto || isPortraitAspectRatio(normalizedAspectRatio)
? webcamLayoutPreset
: DEFAULT_WEBCAM_SETTINGS.layoutPreset;
case "dual-frame":
return isPortraitAspectRatio(normalizedAspectRatio)
return !auto && isPortraitAspectRatio(normalizedAspectRatio)
? DEFAULT_WEBCAM_SETTINGS.layoutPreset
: webcamLayoutPreset;
default:
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/ar/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"blurBg": "تمويه الخلفية",
"help": "تنسيق إطار التسجيل: ضبابية الخلفية، والظل، وضبابية الحركة، واستدارة الزوايا، والحشو حول الفيديو.",
"fitClipOne": "مقطع واحد",
"formatAuto": "تلقائي",
"formatOriginal": "الأصلي",
"fitClipMany": "{{count}} مقاطع",
"frame": "الإطار",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/cs/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"blurBg": "Rozmazat pozadí",
"help": "Stylizace rámce pro nahrávku: rozmazání pozadí, vrhaný stín, pohybové rozmazání, zaoblení rohů a odsazení kolem videa.",
"fitClipOne": "{{count}} klip",
"formatAuto": "Auto",
"formatOriginal": "Původní",
"fitClipMany": "{{count}} klipů",
"frame": "Rám",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/de/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"blurBg": "Hintergrund unscharf",
"help": "Rahmengestaltung der Aufnahme: Hintergrund-Unschärfe, Schlagschatten, Bewegungsunschärfe, Eckenrundung und Abstand um das Video.",
"fitClipOne": "{{count}} Clip",
"formatAuto": "Auto",
"formatOriginal": "Original",
"fitClipMany": "{{count}} Clips",
"frame": "Rahmen",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/en/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"blurBg": "Blur BG",
"help": "Frame styling for the recording: background blur, drop shadow, motion blur, corner radius, and padding around the video.",
"fitClipOne": "{{count}} clip",
"formatAuto": "Auto",
"formatOriginal": "Original",
"fitClipMany": "{{count}} clips",
"frame": "Frame",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/es/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"blurBg": "Desenfocar fondo",
"help": "Estilo del marco de la grabación: desenfoque de fondo, sombra, desenfoque de movimiento, radio de esquinas y margen alrededor del vídeo.",
"fitClipOne": "{{count}} clip",
"formatAuto": "Auto",
"formatOriginal": "Original",
"fitClipMany": "{{count}} clips",
"frame": "Marco",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/fr/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"blurBg": "Flou arrière-plan",
"help": "Mise en forme du cadre de l'enregistrement : flou d'arrière-plan, ombre portée, flou de mouvement, arrondi des coins et marge autour de la vidéo.",
"fitClipOne": "{{count}} clip",
"formatAuto": "Auto",
"formatOriginal": "Original",
"fitClipMany": "{{count}} clips",
"frame": "Cadre",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/it/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"blurBg": "Sfuma sfondo",
"help": "Stile della cornice della registrazione: sfocatura dello sfondo, ombra, sfocatura di movimento, raggio degli angoli e margine attorno al video.",
"fitClipOne": "{{count}} clip",
"formatAuto": "Auto",
"formatOriginal": "Originale",
"fitClipMany": "{{count}} clip",
"frame": "Cornice",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/ja-JP/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"blurBg": "背景をぼかす",
"help": "録画フレームのスタイル設定: 背景ぼかし、ドロップシャドウ、モーションブラー、角丸、動画まわりの余白。",
"fitClipOne": "{{count}} クリップ",
"formatAuto": "自動",
"formatOriginal": "元のサイズ",
"fitClipMany": "{{count}} クリップ",
"frame": "フレーム",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/ko-KR/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"blurBg": "배경 흐림",
"help": "녹화 프레임 스타일: 배경 흐림, 그림자, 모션 블러, 모서리 둥글기, 영상 주변 여백.",
"fitClipOne": "{{count}}개 클립",
"formatAuto": "자동",
"formatOriginal": "원본",
"fitClipMany": "{{count}}개 클립",
"frame": "프레임",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/pt-BR/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"blurBg": "Desfocar Fundo",
"help": "Estilo do quadro da gravação: desfoque de fundo, sombra, desfoque de movimento, raio dos cantos e margem em volta do vídeo.",
"fitClipOne": "{{count}} clipe",
"formatAuto": "Auto",
"formatOriginal": "Original",
"fitClipMany": "{{count}} clipes",
"frame": "Moldura",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/ru/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"blurBg": "Размытие фона",
"help": "Оформление кадра записи: размытие фона, тень, размытие движения, скругление углов и отступ вокруг видео.",
"fitClipOne": "{{count}} клип",
"formatAuto": "Авто",
"formatOriginal": "Исходный",
"fitClipMany": "{{count}} клипов",
"frame": "Рамка",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/tr/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"blurBg": "Arka Planı Bulanıklaştır",
"help": "Kayıt çerçevesinin biçimi: arka plan bulanıklığı, gölge, hareket bulanıklığı, köşe yuvarlaklığı ve videonun çevresindeki boşluk.",
"fitClipOne": "{{count}} klip",
"formatAuto": "Otomatik",
"formatOriginal": "Orijinal",
"fitClipMany": "{{count}} klip",
"frame": "Çerçeve",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/vi/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"blurBg": "Làm mờ nền",
"help": "Kiểu khung của bản ghi: làm mờ nền, đổ bóng, mờ chuyển động, bo góc và khoảng đệm quanh video.",
"fitClipOne": "{{count}} clip",
"formatAuto": "Tự động",
"formatOriginal": "Gốc",
"fitClipMany": "{{count}} clip",
"frame": "Khung",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/zh-CN/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"blurBg": "模糊背景",
"help": "录制画面的边框样式:背景模糊、投影、运动模糊、圆角半径以及视频四周的内边距。",
"fitClipOne": "{{count}} 个片段",
"formatAuto": "自动",
"formatOriginal": "原始",
"fitClipMany": "{{count}} 个片段",
"frame": "画框",
Expand Down
1 change: 1 addition & 0 deletions src/i18n/locales/zh-TW/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"blurBg": "模糊背景",
"help": "錄製畫面的外框樣式:背景模糊、陰影、動態模糊、圓角半徑,以及影片四周的內距。",
"fitClipOne": "{{count}} 個片段",
"formatAuto": "自動",
"formatOriginal": "原始",
"fitClipMany": "{{count}} 個片段",
"frame": "外框",
Expand Down
2 changes: 1 addition & 1 deletion src/lib/ai-edition/captions/captionLane.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ function words(assetId: string, texts: string[]): AxcutTranscript {

function doc(over: Partial<AxcutDocument> = {}): AxcutDocument {
return {
schemaVersion: 7,
schemaVersion: 8,
project: {
id: "p",
title: "T",
Expand Down
2 changes: 1 addition & 1 deletion src/lib/ai-edition/document/audioLanes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ function track(over: Partial<AxcutAudioTrack> & { id: string }): AxcutAudioTrack

function doc(audioTracks: AxcutAudioTrack[]): AxcutDocument {
return {
schemaVersion: 7,
schemaVersion: 8,
project: { id: "p", title: "T", createdAt: "", updatedAt: "" },
assets: [],
transcript: null,
Expand Down
Loading
Loading