Browse Source

fix: address 5 code review findings across execution and UI code

- TutorialOverlay: move completeCurrentStep() inside setTimeout to prevent
  cleanup from clearing the advance timeout, fixing stuck tutorial steps
- useStitchVideos: push silent AudioBuffer for clips with no/incompatible
  audio instead of skipping, preventing audio-video timeline desync
- generate3dExecutor: clear savedFilename/savedFilePath on new generation
  to prevent stale save metadata from previous runs
- pollTaskCompletion: restructure abort listener/timer cleanup to prevent
  listener accumulation across polling iterations
- connectedInputs: exclude loop edges in validateWorkflowPure checks for
  nanoBanana/generateVideo text inputs, matching runtime behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
handoff-20260429-1057
shrimbly 3 months ago
parent
commit
dfaa684748
  1. 7
      src/components/onboarding/TutorialOverlay.tsx
  2. 17
      src/hooks/useStitchVideos.ts
  3. 2
      src/store/execution/generate3dExecutor.ts
  4. 19
      src/store/execution/pollTaskCompletion.ts
  5. 3
      src/store/utils/connectedInputs.ts

7
src/components/onboarding/TutorialOverlay.tsx

@ -112,12 +112,15 @@ export function TutorialOverlay() {
} }
if (actionCompleted) { if (actionCompleted) {
completeCurrentStep(); // Advance to next step after configurable delay (default 1000ms).
// Advance to next step after configurable delay (default 1000ms) // Both completeCurrentStep() and nextTutorialStep() are called inside
// the timeout so that the state update doesn't trigger a re-render
// whose cleanup would clear the pending timeout.
const delay = currentStep.advanceDelay !== undefined ? currentStep.advanceDelay : 1000; const delay = currentStep.advanceDelay !== undefined ? currentStep.advanceDelay : 1000;
if (advanceTimeoutRef.current) clearTimeout(advanceTimeoutRef.current); if (advanceTimeoutRef.current) clearTimeout(advanceTimeoutRef.current);
advanceTimeoutRef.current = setTimeout(() => { advanceTimeoutRef.current = setTimeout(() => {
advanceTimeoutRef.current = null; advanceTimeoutRef.current = null;
completeCurrentStep();
nextTutorialStep(); nextTutorialStep();
}, delay); }, delay);
} }

17
src/hooks/useStitchVideos.ts

@ -224,13 +224,14 @@ export async function stitchVideosAsync(
try { try {
const blobSource = new BlobSource(videoBlobs[i]); const blobSource = new BlobSource(videoBlobs[i]);
const input = new Input({ source: blobSource, formats: ALL_FORMATS }); const input = new Input({ source: blobSource, formats: ALL_FORMATS });
let extractedAudio = false;
try { try {
const clipDuration = await input.computeDuration();
const audioTracks = await input.getAudioTracks(); const audioTracks = await input.getAudioTracks();
if (audioTracks.length > 0) { if (audioTracks.length > 0) {
const audioTrack = audioTracks[0]; const audioTrack = audioTracks[0];
const sink = new AudioBufferSink(audioTrack); const sink = new AudioBufferSink(audioTrack);
const duration = await input.computeDuration(); for await (const wrapped of sink.buffers(0, clipDuration)) {
for await (const wrapped of sink.buffers(0, duration)) {
if (referenceSampleRate === null) { if (referenceSampleRate === null) {
referenceSampleRate = wrapped.buffer.sampleRate; referenceSampleRate = wrapped.buffer.sampleRate;
referenceChannels = wrapped.buffer.numberOfChannels; referenceChannels = wrapped.buffer.numberOfChannels;
@ -240,9 +241,21 @@ export async function stitchVideosAsync(
wrapped.buffer.numberOfChannels === referenceChannels wrapped.buffer.numberOfChannels === referenceChannels
) { ) {
allAudioBuffers.push(wrapped.buffer); allAudioBuffers.push(wrapped.buffer);
extractedAudio = true;
} }
} }
} }
// If no audio was extracted (no tracks or incompatible params),
// push a silent buffer to maintain timeline alignment
if (!extractedAudio && referenceSampleRate && referenceChannels && clipDuration > 0) {
const silentSamples = Math.max(1, Math.floor(clipDuration * referenceSampleRate));
const silentBuffer = new AudioBuffer({
length: silentSamples,
numberOfChannels: referenceChannels,
sampleRate: referenceSampleRate,
});
allAudioBuffers.push(silentBuffer);
}
} finally { } finally {
input.dispose(); input.dispose();
} }

2
src/store/execution/generate3dExecutor.ts

@ -137,6 +137,8 @@ export async function executeGenerate3D(
output3dUrl: result.model3dUrl, output3dUrl: result.model3dUrl,
status: "complete", status: "complete",
error: null, error: null,
savedFilename: null,
savedFilePath: null,
}); });
// Track cost if applicable // Track cost if applicable

19
src/store/execution/pollTaskCompletion.ts

@ -50,19 +50,16 @@ export async function pollGenerateTask(
// Wait before polling // Wait before polling
await new Promise<void>((resolve, reject) => { await new Promise<void>((resolve, reject) => {
const timer = setTimeout(resolve, interval); const onAbort = () => {
clearTimeout(timer);
reject(new DOMException("The operation was aborted.", "AbortError"));
};
const timer = setTimeout(() => {
if (signal) signal.removeEventListener("abort", onAbort);
resolve();
}, interval);
if (signal) { if (signal) {
const onAbort = () => {
clearTimeout(timer);
reject(new DOMException("The operation was aborted.", "AbortError"));
};
signal.addEventListener("abort", onAbort, { once: true }); signal.addEventListener("abort", onAbort, { once: true });
// Clean up listener when timer fires
const origResolve = resolve;
resolve = () => {
signal.removeEventListener("abort", onAbort);
origResolve();
};
} }
}); });

3
src/store/utils/connectedInputs.ts

@ -403,11 +403,13 @@ export function validateWorkflowPure(
} }
// Check each Nano Banana node has required inputs (text required, image optional) // Check each Nano Banana node has required inputs (text required, image optional)
// Loop edges are excluded because they carry no data on the first iteration.
nodes nodes
.filter((n) => n.type === "nanoBanana") .filter((n) => n.type === "nanoBanana")
.forEach((node) => { .forEach((node) => {
const textConnected = edges.some( const textConnected = edges.some(
(e) => e.target === node.id && (e) => e.target === node.id &&
!e.data?.isLoop &&
(e.targetHandle === "text" || e.targetHandle?.startsWith("text-")) (e.targetHandle === "text" || e.targetHandle?.startsWith("text-"))
); );
if (!textConnected) { if (!textConnected) {
@ -421,6 +423,7 @@ export function validateWorkflowPure(
.forEach((node) => { .forEach((node) => {
const textConnected = edges.some( const textConnected = edges.some(
(e) => e.target === node.id && (e) => e.target === node.id &&
!e.data?.isLoop &&
(e.targetHandle === "text" || e.targetHandle?.startsWith("text-")) (e.targetHandle === "text" || e.targetHandle?.startsWith("text-"))
); );
if (!textConnected) { if (!textConnected) {

Loading…
Cancel
Save