Skip to content

Commit 32ce779

Browse files
committed
fix: stream slide summaries before frame extraction
1 parent 881a1cf commit 32ce779

7 files changed

Lines changed: 537 additions & 22 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
### Fixes
66

7+
- Slides: seed planned slide timelines from transcript duration so YouTube slide summaries can start streaming before video frame extraction finishes.
78
- Chrome extension: keep partial streamed slide-summary fragments from blocking the final summary, so YouTube slide cards replace transcript snippets with the same slide summaries the CLI renders.
89
- Chrome extension: render slide-summary intro text above gallery cards and remove the duplicate Slides count heading so sidebar slide output matches the CLI shape.
910
- Release: harden npm publishing so raw `npm publish` is blocked, packed manifests reject `workspace:*`, and releases publish to `next` before exact-version smoke promotes `latest`.

apps/chrome-extension/src/entrypoints/sidepanel/main.ts

Lines changed: 92 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,7 @@ const slidesSession = createSlidesSessionStore({
241241
const slidesState = slidesSession.state;
242242
const pendingSummaryRunsByUrl = new Map<string, RunStart>();
243243
const pendingSlidesRunsByUrl = new Map<string, { runId: string; url: string }>();
244+
let lastPlannedSlidesRun: RunStart | null = null;
244245
const slidesTextController = createSlidesTextController({
245246
getSlides: () => panelState.slides?.slides ?? null,
246247
getLengthValue: () => appearanceControls.getLengthValue(),
@@ -375,24 +376,79 @@ function attachSummaryRun(run: RunStart) {
375376
};
376377
}
377378
slidesState.pendingRunForPlannedSlides = runRequestsSlides ? run : null;
378-
if (!panelState.summaryMarkdown?.trim()) {
379-
renderMarkdownDisplay();
380-
}
379+
lastPlannedSlidesRun = runRequestsSlides ? run : null;
381380
if (runRequestsSlides) {
382381
startSlidesStream(run);
382+
seedPlannedSlidesForRun(run);
383+
}
384+
if (!panelState.summaryMarkdown?.trim()) {
385+
renderMarkdownDisplay();
383386
}
384387
void streamController.start(run);
385388
}
386389

387390
function maybeSeedPlannedSlidesForPendingRun() {
388-
if (!slidesState.pendingRunForPlannedSlides) return false;
389-
if (seedPlannedSlidesForRun(slidesState.pendingRunForPlannedSlides)) {
390-
slidesState.pendingRunForPlannedSlides = null;
391+
const pendingRun = getPlannedSlidesRunForReseed();
392+
if (!pendingRun) return false;
393+
if (!isCurrentPlannedSlidesRun(pendingRun)) return false;
394+
if (seedPlannedSlidesForRun(pendingRun)) {
395+
if (plannedSlidesHaveUsableTimingOrImages(pendingRun)) {
396+
clearPlannedSlidesRunForReseed();
397+
}
391398
return true;
392399
}
393400
return false;
394401
}
395402

403+
function getPlannedSlidesRunForReseed() {
404+
if (slidesState.pendingRunForPlannedSlides) return slidesState.pendingRunForPlannedSlides;
405+
if (!lastPlannedSlidesRun) return null;
406+
return panelState.runId === lastPlannedSlidesRun.id ||
407+
panelState.slidesRunId === lastPlannedSlidesRun.id
408+
? lastPlannedSlidesRun
409+
: null;
410+
}
411+
412+
function clearPlannedSlidesRunForReseed() {
413+
slidesState.pendingRunForPlannedSlides = null;
414+
lastPlannedSlidesRun = null;
415+
}
416+
417+
function plannedSlidesHaveUsableTimingOrImages(run: RunStart) {
418+
if (currentRunHasResolvedSlideImages(run)) return true;
419+
const sourceId = getPlannedSlidesSourceId(run);
420+
if (!panelState.slides || panelState.slides.sourceId !== sourceId) return false;
421+
return panelState.slides.slides.some(
422+
(slide) => Number.isFinite(slide.timestamp) || (slide.imageUrl ?? "").trim().length > 0,
423+
);
424+
}
425+
426+
function currentRunHasResolvedSlideImages(run: RunStart) {
427+
if (!panelState.slides) return false;
428+
const hasResolvedImages = panelState.slides.slides.some(
429+
(slide) => (slide.imageUrl ?? "").trim().length > 0,
430+
);
431+
if (!hasResolvedImages) return false;
432+
if (panelState.runId === run.id || panelState.slidesRunId === run.id) return true;
433+
const sourceUrl = panelState.slides.sourceUrl || panelState.currentSource?.url || activeTabUrl;
434+
return sourceUrl ? panelUrlsMatch(run.url, sourceUrl) : false;
435+
}
436+
437+
function seedPlannedSlidesForPendingRunAndConsumeWhenReady() {
438+
const pendingRun = getPlannedSlidesRunForReseed();
439+
if (!pendingRun) return;
440+
if (!isCurrentPlannedSlidesRun(pendingRun)) return;
441+
if (seedPlannedSlidesForRun(pendingRun) && plannedSlidesHaveUsableTimingOrImages(pendingRun)) {
442+
clearPlannedSlidesRunForReseed();
443+
}
444+
}
445+
446+
function isCurrentPlannedSlidesRun(run: RunStart) {
447+
const currentUrl =
448+
panelState.currentSource?.url ?? activeTabUrl ?? panelState.ui?.tab.url ?? null;
449+
return currentUrl ? panelUrlsMatch(run.url, currentUrl) : false;
450+
}
451+
396452
function showAutomationNotice({
397453
title,
398454
message,
@@ -1168,10 +1224,7 @@ const summaryStreamRuntime = createSummaryStreamRuntime({
11681224
panelCacheController.scheduleSync();
11691225
},
11701226
seedPlannedSlidesForPendingRun: () => {
1171-
if (slidesState.pendingRunForPlannedSlides) {
1172-
seedPlannedSlidesForRun(slidesState.pendingRunForPlannedSlides);
1173-
slidesState.pendingRunForPlannedSlides = null;
1174-
}
1227+
seedPlannedSlidesForPendingRunAndConsumeWhenReady();
11751228
},
11761229
setSlidesBusy,
11771230
setPhase,
@@ -1478,9 +1531,16 @@ summarizeControlRuntime = createSummarizeControlRuntime({
14781531
},
14791532
});
14801533

1534+
function getPlannedSlidesSourceId(run: RunStart) {
1535+
const youtubeId = extractYouTubeVideoId(run.url);
1536+
return youtubeId ? `youtube-${youtubeId}` : `planned-${run.id}`;
1537+
}
1538+
14811539
function seedPlannedSlidesForRun(run: RunStart) {
14821540
const durationSeconds =
14831541
slidesState.summarizeVideoDurationSeconds ?? panelState.ui?.stats.videoDurationSeconds ?? null;
1542+
const hasDuration =
1543+
typeof durationSeconds === "number" && Number.isFinite(durationSeconds) && durationSeconds > 0;
14841544
if (
14851545
!shouldSeedPlannedSlidesForRun({
14861546
durationSeconds,
@@ -1508,24 +1568,37 @@ function seedPlannedSlidesForRun(run: RunStart) {
15081568
? 120
15091569
: 300;
15101570

1511-
const target = Math.max(3, Math.round(durationSeconds / chunkSeconds));
1571+
const defaultCount = 6;
1572+
const target = hasDuration
1573+
? Math.max(3, Math.round(durationSeconds / chunkSeconds))
1574+
: defaultCount;
15121575
const count = Math.max(3, Math.min(80, target));
15131576

15141577
const youtubeId = extractYouTubeVideoId(run.url);
1515-
const sourceId = youtubeId ? `youtube-${youtubeId}` : `planned-${run.id}`;
1578+
const sourceId = getPlannedSlidesSourceId(run);
15161579
const sourceKind = youtubeId ? "youtube" : "direct";
1517-
1518-
if (
1519-
panelState.slides &&
1520-
panelState.slides.sourceId === sourceId &&
1521-
panelState.slides.slides.length > 0
1522-
) {
1580+
if (currentRunHasResolvedSlideImages(run)) {
15231581
return true;
15241582
}
15251583

1584+
const existingSlides = panelState.slides?.sourceId === sourceId ? panelState.slides : null;
1585+
if (existingSlides && existingSlides.slides.length > 0) {
1586+
const hasResolvedImages = existingSlides.slides.some(
1587+
(slide) => (slide.imageUrl ?? "").trim().length > 0,
1588+
);
1589+
const hasUsableTimestamps = existingSlides.slides.some((slide) =>
1590+
Number.isFinite(slide.timestamp),
1591+
);
1592+
if (hasResolvedImages || !hasDuration || hasUsableTimestamps) {
1593+
return true;
1594+
}
1595+
}
1596+
15261597
const slides = Array.from({ length: count }, (_, i) => {
15271598
const ratio = count <= 1 ? 0 : i / Math.max(1, count - 1);
1528-
const timestamp = Math.max(0, Math.min(durationSeconds - 0.1, ratio * durationSeconds));
1599+
const timestamp = hasDuration
1600+
? Math.max(0, Math.min(durationSeconds - 0.1, ratio * durationSeconds))
1601+
: Number.NaN;
15291602
const index = i + 1;
15301603
return { index, timestamp, imageUrl: "" };
15311604
});

apps/chrome-extension/src/entrypoints/sidepanel/slides-seed-policy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export function shouldSeedPlannedSlidesForRun({
1717
slidesEnabled: boolean;
1818
}) {
1919
if (!slidesEnabled) return false;
20-
if (!durationSeconds || !Number.isFinite(durationSeconds) || durationSeconds <= 0) return false;
20+
void durationSeconds;
2121
if (inputMode === "video") return true;
2222
if (mediaAvailable) return true;
2323
if (media?.hasVideo || media?.hasAudio) return true;

0 commit comments

Comments
 (0)