Skip to content

Commit 0ec429d

Browse files
committed
fix(studio): single-frame export uses wrong content, ignores duration setting
1. Wrong content: exportMp4 always rendered the template's original source HTML, ignoring the agent-generated preview saved to lastPreviewHtmlPath. Now uses lastPreviewHtmlPath when present. 2. Duration ignored: exportMp4 hardcoded `duration: 'auto'`, causing the adapter to fall back to the animation's natural length (~4s) instead of the user's setting. durationTargetSec was defined in UserPreferences but never written or read. - studio-server now persists collected.duration to preferences.durationTargetSec at generate time (covers both single-frame and multi-frame paths). - exportMp4 reads durationTargetSec and passes it as durationMode: 'explicit' so the adapter honors it as a hard cap. 3. UI showed render time not video duration: the "MP4 已导出 · Xs" badge was displaying elapsed_ms (wall-clock render time) instead of the actual video length. export_done SSE event now carries duration_sec; the studio UI uses that instead.
1 parent 90a036a commit 0ec429d

3 files changed

Lines changed: 27 additions & 4 deletions

File tree

packages/cli/src/studio-server.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -398,7 +398,11 @@ export async function startStudioServer(ctx: CliContext, port: number): Promise<
398398
process.stderr.write(
399399
`[studio:export] proj=${projectId} done in ${ms}ms → ${outputPath}\n`,
400400
);
401-
sse({ type: 'export_done', output_path: outputPath, project, elapsed_ms: ms });
401+
// Compute the actual video duration to show in the UI (not the render wall-clock time).
402+
const videoDurationSec = (project.frames && project.frames.length > 0)
403+
? project.frames.reduce((s, f) => s + (f.durationSec || 0), 0)
404+
: (project.preferences.durationTargetSec ?? null);
405+
sse({ type: 'export_done', output_path: outputPath, project, elapsed_ms: ms, duration_sec: videoDurationSec });
402406
} catch (err) {
403407
const msg = err instanceof Error ? err.message : String(err);
404408
process.stderr.write(`[studio:export] proj=${projectId} failed: ${msg}\n`);
@@ -958,6 +962,16 @@ export async function startStudioServer(ctx: CliContext, port: number): Promise<
958962
let textChunks = 0;
959963
let summaryLine = '';
960964

965+
// Persist user duration preference at generate time so exportMp4 can
966+
// honor it regardless of single-frame vs multi-frame path.
967+
if (phaseInfo.phase === 'generate' && phaseInfo.inputs.collected) {
968+
const parsedDur = Number(phaseInfo.inputs.collected.duration ?? '') || 0;
969+
if (parsedDur > 0) {
970+
project.preferences = { ...project.preferences, durationTargetSec: parsedDur };
971+
await ctx.projects.save(project);
972+
}
973+
}
974+
961975
// ---- generate-phase: multi-frame path runs split (graph + per-frame) ----
962976
// Empirically claude --print returns 1 byte ~50% of the time when asked
963977
// to emit a graph and 4-6 full HTML pages in a single response. Each

packages/core/src/project.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -443,15 +443,24 @@ export class ProjectOrchestrator {
443443
const tmpl = this.deps.templates.get(project.templateId);
444444
const adapter = this.deps.engines.get(tmpl.engine);
445445

446+
// Use the agent-generated HTML if available; fall back to the template source.
447+
const singleFrameTemplateRef = project.lastPreviewHtmlPath
448+
? { id: tmpl.id, engine: tmpl.engine, sourcePath: project.lastPreviewHtmlPath }
449+
: templateRefFromMeta(tmpl);
450+
// Honor the user-set duration; fall back to 'auto' so the adapter probes animation length.
451+
const singleFrameDuration = project.preferences.durationTargetSec ?? 'auto';
452+
const singleFrameDurationMode = typeof singleFrameDuration === 'number' ? 'explicit' as const : undefined;
453+
446454
await adapter.render(
447455
{
448-
template: templateRefFromMeta(tmpl),
456+
template: singleFrameTemplateRef,
449457
variables: project.variables,
450458
config: {
451459
format: 'mp4',
452460
resolution: project.preferences.resolution ?? { width: 1920, height: 1080 },
453461
fps: project.preferences.fps ?? 60,
454-
duration: 'auto',
462+
duration: singleFrameDuration,
463+
...(singleFrameDurationMode && { durationMode: singleFrameDurationMode }),
455464
outputPath,
456465
},
457466
},

packages/project-studio/public/app.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ async function startExportStream() {
192192
state.exporting = false;
193193
state.exportProgress = null;
194194
if (ev.project) state.selected = ev.project;
195-
const seconds = ev.elapsed_ms ? `${(ev.elapsed_ms / 1000).toFixed(1)}s` : '';
195+
const seconds = ev.duration_sec != null ? `${Number(ev.duration_sec).toFixed(1)}s` : '';
196196
state.messages.push({
197197
role: 'preview-event',
198198
content: seconds ? t('export.done_seconds', { seconds }) : t('export.done_no_seconds'),

0 commit comments

Comments
 (0)