Merge branch 'codex/20260818-video-confirm-media-9b7c-video-confirm-media'
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
# Task: Fix video intent rendering as image confirmation
|
||||
|
||||
## Identity
|
||||
|
||||
- Task ID: 20260818-video-confirm-media-9b7c
|
||||
- Mode: Feature
|
||||
- Branch: codex/20260818-video-confirm-media-9b7c-video-confirm-media
|
||||
- Worktree: D:\Datas\OthersProjects\makelore-video-confirm-media-9b7c
|
||||
- Base commit: 11b19832a35477d2136c6ea953dd9c408fd84816
|
||||
- Owner: codex
|
||||
- Status: Ready for Integration
|
||||
|
||||
## Scope
|
||||
|
||||
- Normalize the server's public generation-option payload for the Electron/Main-to-Renderer shared design contract.
|
||||
- Support both canonical client options (`value`/`disabled`) and the current server configuration shape (`id` or `seconds`/`enabled`) without changing the Renderer API.
|
||||
- Add a regression proving a video Quote retains its medium and exposes selectable 2/6 second durations.
|
||||
|
||||
## Intent And Constraints
|
||||
|
||||
- The raw trace proves this run reached `medium=video` and `bailian_video_direct_v1`; the UI failure was caused by option-shape loss, not by a server image decision.
|
||||
- Preserve existing image and legacy conversation compatibility.
|
||||
- Do not expose provider credentials or change the HTTP API; normalization belongs at the Electron adapter boundary.
|
||||
|
||||
## Outcome
|
||||
|
||||
- Added a typed adapter normalizer that maps `value`/`id`/`seconds` to the shared `value` field and maps `enabled=false` to `disabled=true` (while retaining canonical payloads).
|
||||
- The Renderer now receives video durations as numeric selectable options, so the duration control and video confirmation validation can work with the live server response.
|
||||
- Added a regression test using the server-shaped video options and asserting the mapped video medium, model, and durations.
|
||||
|
||||
## Verification
|
||||
|
||||
- `pnpm exec vitest run tests/unit/works-square-design-workspace.test.ts` — 32 passed.
|
||||
- `pnpm exec vitest run tests/unit/image-canvas-page.test.tsx tests/unit/works-square-design-workspace.test.ts` — 79 passed.
|
||||
- `pnpm run typecheck` — passed.
|
||||
- `pnpm exec eslint electron/image-workspace/works-square-workspace.ts tests/unit/works-square-design-workspace.test.ts` — passed.
|
||||
- `pnpm run build:vite` — passed (existing chunk-size and dynamic-import warnings only).
|
||||
- `git diff --check` — passed (only the repository's CRLF conversion warnings).
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- Ship this client commit with the server intent-guard commit; run the packaged desktop smoke test against a live video Quote.
|
||||
- A future API contract cleanup may make the server publish canonical option names directly, but the adapter normalizer is the compatibility boundary for now.
|
||||
|
||||
## Promotion Candidates
|
||||
|
||||
- None recorded.
|
||||
@@ -45,6 +45,17 @@ type ServerBrief = {
|
||||
missing_decision: string | null;
|
||||
};
|
||||
|
||||
type ServerGenerationOption = {
|
||||
value?: string | number;
|
||||
id?: string | number;
|
||||
seconds?: number;
|
||||
label?: string;
|
||||
default?: boolean;
|
||||
disabled?: boolean;
|
||||
enabled?: boolean;
|
||||
multiplier?: number;
|
||||
};
|
||||
|
||||
type ServerQuote = {
|
||||
quote_id: string;
|
||||
status: DesignGenerationQuote['status'];
|
||||
@@ -60,10 +71,10 @@ type ServerQuote = {
|
||||
duration_seconds: number | null;
|
||||
};
|
||||
generation_options: {
|
||||
models: Array<DesignGenerationOption<string>>;
|
||||
resolutions: Array<DesignGenerationOption<string>>;
|
||||
durations: Array<DesignGenerationOption<number>>;
|
||||
aspect_ratios: Array<DesignGenerationOption<string>>;
|
||||
models: ServerGenerationOption[];
|
||||
resolutions: ServerGenerationOption[];
|
||||
durations: ServerGenerationOption[];
|
||||
aspect_ratios: ServerGenerationOption[];
|
||||
};
|
||||
pricing: {
|
||||
schema: string;
|
||||
@@ -281,6 +292,22 @@ function mapBrief(brief: ServerBrief): DesignBrief {
|
||||
};
|
||||
}
|
||||
|
||||
function mapGenerationOptions<TValue extends string | number>(
|
||||
options: ServerGenerationOption[] | undefined,
|
||||
): DesignGenerationOption<TValue>[] {
|
||||
return (options ?? []).flatMap((option) => {
|
||||
const value = option.value ?? option.id ?? option.seconds;
|
||||
if (typeof value !== 'string' && typeof value !== 'number') return [];
|
||||
return [{
|
||||
value: value as TValue,
|
||||
label: option.label ?? String(value),
|
||||
default: option.default ?? false,
|
||||
disabled: option.disabled ?? option.enabled === false,
|
||||
multiplier: option.multiplier ?? 1,
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
function mapQuote(quote: ServerQuote | null): DesignGenerationQuote | null {
|
||||
if (!quote) return null;
|
||||
return {
|
||||
@@ -298,10 +325,10 @@ function mapQuote(quote: ServerQuote | null): DesignGenerationQuote | null {
|
||||
durationSeconds: quote.generation_parameters.duration_seconds,
|
||||
},
|
||||
generationOptions: {
|
||||
models: quote.generation_options.models,
|
||||
resolutions: quote.generation_options.resolutions,
|
||||
durations: quote.generation_options.durations,
|
||||
aspectRatios: quote.generation_options.aspect_ratios,
|
||||
models: mapGenerationOptions<string>(quote.generation_options.models),
|
||||
resolutions: mapGenerationOptions<string>(quote.generation_options.resolutions),
|
||||
durations: mapGenerationOptions<number>(quote.generation_options.durations),
|
||||
aspectRatios: mapGenerationOptions<string>(quote.generation_options.aspect_ratios),
|
||||
},
|
||||
pricing: {
|
||||
schema: quote.pricing.schema,
|
||||
|
||||
@@ -592,6 +592,53 @@ describe('Works Square AI design adapter', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('normalizes the server video option shape so the duration picker stays available', async () => {
|
||||
const videoConversation = {
|
||||
...serverConversation,
|
||||
brief: { ...serverConversation.brief, medium: 'video' },
|
||||
messages: serverConversation.messages.map((message) => ({
|
||||
...message,
|
||||
generation_quote: {
|
||||
...message.generation_quote!,
|
||||
medium: 'video',
|
||||
generation_parameters: {
|
||||
...message.generation_quote!.generation_parameters,
|
||||
model: 'wan2.7-i2v-2026-04-25',
|
||||
resolution: '720P',
|
||||
aspect_ratio: '4:5',
|
||||
duration_seconds: 4,
|
||||
},
|
||||
generation_options: {
|
||||
models: [{ id: 'wan2.7-i2v-2026-04-25', label: '万相视频', enabled: true, multiplier: 1 }],
|
||||
resolutions: [{ value: '720P', label: '720P', enabled: true, multiplier: 1 }],
|
||||
durations: [
|
||||
{ seconds: 2, label: '2 秒', enabled: true, multiplier: 1 },
|
||||
{ seconds: 6, label: '6 秒', enabled: true, multiplier: 1 },
|
||||
],
|
||||
aspect_ratios: [{ value: '4:5', label: '4:5', default: true, enabled: true, multiplier: 1 }],
|
||||
},
|
||||
},
|
||||
})),
|
||||
};
|
||||
const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce(jsonResponse(videoConversation));
|
||||
const adapter = new WorksSquareDesignWorkspace({
|
||||
apiBaseUrl: 'https://square.example',
|
||||
fetchImpl: fetchMock,
|
||||
});
|
||||
|
||||
const mapped = await adapter.getConversation('workspace-one', 'conversation-one');
|
||||
const quote = mapped.messages[0].generationQuote!;
|
||||
|
||||
expect(mapped.brief.medium).toBe('video');
|
||||
expect(quote.medium).toBe('video');
|
||||
expect(quote.generationOptions.models[0]).toMatchObject({
|
||||
value: 'wan2.7-i2v-2026-04-25',
|
||||
disabled: false,
|
||||
});
|
||||
expect(quote.generationOptions.durations.map((option) => option.value)).toEqual([2, 6]);
|
||||
expect(quote.generationOptions.durations.every((option) => !option.disabled)).toBe(true);
|
||||
});
|
||||
|
||||
it('uploads a local reference image as multipart data and maps the returned Asset', async () => {
|
||||
const fetchMock = vi.fn<typeof fetch>().mockResolvedValueOnce(jsonResponse({
|
||||
asset_id: 'asset-uploaded',
|
||||
|
||||
Reference in New Issue
Block a user