diff --git a/content.js b/content.js index 202a154..17c660e 100644 --- a/content.js +++ b/content.js @@ -37,13 +37,18 @@ let successCount = 0; let consecutiveRateLimits = 0; function init() { - chrome.storage.local.get(settings, (data) => { + chrome.storage.local.get({ ...settings, pendingHarvest: null }, (data) => { + const ph = data.pendingHarvest; delete data.pendingHarvest; settings = { ...settings, ...data }; if (settings.autoStart && !settings.isRunning) { settings.isRunning = true; chrome.storage.local.set({ isRunning: true }); // so the popup reflects it } log('system', `Content script connected (${settings.isRunning ? 'ACTIVE' : 'idle'}).`); + if (ph) { // a video finished right before the crucial post-gen refresh — harvest it now + runPostReloadHarvest(ph).then(() => { if (settings.isRunning) startPolling(); }); + return; + } if (settings.isRunning) startPolling(); }); } @@ -216,6 +221,15 @@ function persistentError() { (e.textContent || '').length < 180); } +// The agent's end-of-generation chatter. John's law: this message LIES — "scheduled / +// waiting in the queue due to high demand" often means it's actually DONE, and the +// stale page won't show the asset until a refresh. +function agentSaysDone() { + return [...document.querySelectorAll('*')].some(e => visible(e) && + /has been scheduled|waiting in the queue|check back in a few|is ready|i'?ve (generated|created)|here('s| is) (your|the) video/i.test(e.textContent || '') && + (e.textContent || '').length < 240); +} + // Flow's "Try again" button, shown beside the "Something went wrong" banner. // Clicking it re-runs the SAME prompt (no re-injection needed) — the manual fix. function findTryAgain() { @@ -354,8 +368,36 @@ async function executeTask(task) { log('info', 'Submitted (stop square seen). Waiting for generation…'); - // Wait for a new asset, a Flow error (fast-fail), or timeout. Images should be quick. const timeoutMs = task.kind === 'image' ? 3 * 60 * 1000 : settings.genTimeoutMs; + + if (task.kind === 'video') { + // John's law: after a video gen the page is stale AND the agent's completion + // message lies ("queued due to high demand" = usually done). The post-gen HARD + // REFRESH is crucial. Wait for a done-ish signal, let it settle, persist the + // harvest state (so the reload can't re-submit and burn credits), then reload. + const t0 = Date.now(); + let signal = null; + while (Date.now() - t0 < timeoutMs) { + if (snapshotAssets().size > before.size) { signal = 'asset in grid'; break; } + if (agentSaysDone()) { signal = 'agent completion message'; break; } + if (Date.now() - t0 > 6000 && persistentError()) { await handleRateLimit(task); return; } + if (Date.now() - t0 > 8000 && (findTryAgain() || flowError())) + return await requeueWithReload(task, 'Flow errored during video generation'); + await sleep(2500); + } + if (!signal) throw new Error('Timed out — no completion signal for video.'); + log('info', `Video done (${signal}). Settling 2s, then the crucial post-gen hard refresh…`); + await sleep(2500); + await new Promise(r => chrome.storage.local.set({ pendingHarvest: { + id: task.id, kind: task.kind, category: task.category, tries: 0, t: Date.now(), + before: [...before].filter(u => /^https?:/.test(u)), // blob: urls don't survive reloads + } }, r)); + const r = await bg({ type: 'hardReload' }); + if (!r || !r.ok) location.reload(); + return; // the fresh page harvests via runPostReloadHarvest + } + + // Images: the grid updates live — wait in-page for a new asset, error, or timeout. const outcome = await waitForResultOrError(before.size, timeoutMs); if (outcome === 'ratelimit') { await handleRateLimit(task); return; } // long back-off, don't hammer if (outcome === 'error') return await requeueWithReload(task, '"Something went wrong" persisted through Try-again'); @@ -396,6 +438,56 @@ async function executeTask(task) { } } +// After the crucial post-gen refresh: diff the fresh grid against the persisted +// pre-submit snapshot, download what's new, report. Retries the refresh twice if +// the asset still hasn't surfaced; never re-submits (that would double-bill credits). +async function runPostReloadHarvest(ph) { + generationInProgress = true; + try { + if (Date.now() - (ph.t || 0) > 30 * 60 * 1000) { // stale (browser was closed?) — don't guess + chrome.storage.local.remove('pendingHarvest'); + await reportCompletion(ph.id, 'failed', 'post-gen harvest state went stale (>30 min old)', []); + return; + } + await waitForAgentReady(30000); + const before = new Set(ph.before || []); + await waitForStable(() => [...snapshotAssets()].filter(u => /^https?:/.test(u)).length, + { maxMs: 90000, min: before.size + 1 }); + const fresh = [...snapshotAssets()].filter(u => /^https?:/.test(u) && !before.has(u)); + if (!fresh.length) { + if ((ph.tries || 0) < 2) { + ph.tries = (ph.tries || 0) + 1; + await new Promise(r => chrome.storage.local.set({ pendingHarvest: ph }, r)); + log('info', `Post-refresh grid shows nothing new yet — refresh ${ph.tries}/2 and re-check.`); + await sleep(10000); + const r = await bg({ type: 'hardReload' }); + if (!r || !r.ok) location.reload(); + return; + } + chrome.storage.local.remove('pendingHarvest'); + await reportCompletion(ph.id, 'failed', 'video generated but never surfaced in the grid after 3 loads', []); + return; + } + const cat = (ph.category || 'misc').replace(/[^a-z0-9_\-]/gi, '_'); + let saved = 0; + for (let i = 0; i < fresh.length; i++) { + const r = await bg({ type: 'download', url: fresh[i], filename: `flowrinse/${cat}/${ph.id}_${i}.${ph.kind === 'video' ? 'mp4' : 'png'}` }); + if (r && r.ok) saved++; + } + log('success', `Post-refresh harvest: ${fresh.length} asset(s), downloaded ${saved} → Downloads/flowrinse/${cat}/.`); + chrome.storage.local.remove('pendingHarvest'); + chrome.storage.local.remove('requeue_' + ph.id); + await reportCompletion(ph.id, 'success', null, fresh); + successCount++; consecutiveRateLimits = 0; + } catch (err) { + log('error', `Post-refresh harvest failed: ${err.message}`); + chrome.storage.local.remove('pendingHarvest'); + await reportCompletion(ph.id, 'failed', `post-refresh harvest: ${err.message}`, []); + } finally { + generationInProgress = false; + } +} + async function reportCompletion(id, status, error, assets) { try { await fetch(`${settings.serverUrl}/task-completed`, {