Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 28 additions & 18 deletions apps/web/actions/videos/get-status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,35 +77,45 @@ export async function getVideoStatus(
console.log(
`[Get Status] Transcription not started for video ${videoId}, triggering transcription`,
);
try {
transcribeVideo(videoId, video.ownerId).catch((error) => {
console.error(
`[Get Status] Error starting transcription for video ${videoId}:`,
error,
);
});

return {
transcriptionStatus: "PROCESSING",
aiGenerationStatus:
(metadata.aiGenerationStatus as AiGenerationStatus) || null,
aiTitle: metadata.aiTitle || null,
summary: metadata.summary || null,
chapters: metadata.chapters || null,
};
} catch (error) {
await db()
.update(videos)
.set({ transcriptionStatus: "PROCESSING" })
.where(eq(videos.id, videoId));
Comment on lines +81 to +84
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

race condition: transcribeVideo on line 86 also sets PROCESSING at line 143, creating duplicate DB writes. Since transcribeVideo is called with _isRetry=true, it will skip the PROCESSING check anyway

Suggested change
await db()
.update(videos)
.set({ transcriptionStatus: "PROCESSING" })
.where(eq(videos.id, videoId));
transcribeVideo(videoId, video.ownerId, false, true).catch((error) => {
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/web/actions/videos/get-status.ts
Line: 81-84

Comment:
race condition: `transcribeVideo` on line 86 also sets PROCESSING at line 143, creating duplicate DB writes. Since `transcribeVideo` is called with `_isRetry=true`, it will skip the PROCESSING check anyway

```suggestion
		transcribeVideo(videoId, video.ownerId, false, true).catch((error) => {
```

How can I resolve this? If you propose a fix, please make it concise.


transcribeVideo(videoId, video.ownerId, false, true).catch((error) => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

transcribeVideo(videoId, video.ownerId, false, true) is pretty hard to read/maintain (easy to swap booleans accidentally). Would you be open to at least naming the flags at the callsite?

Suggested change
transcribeVideo(videoId, video.ownerId, false, true).catch((error) => {
const aiGenerationEnabled = false;
const isRetry = true;
transcribeVideo(videoId, video.ownerId, aiGenerationEnabled, isRetry).catch((error) => {

console.error(
`[Get Status] Error triggering transcription for video ${videoId}:`,
`[Get Status] Error starting transcription for video ${videoId}:`,
error,
);
});
Comment on lines +81 to +91
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Race condition: Setting PROCESSING status in the database before firing the async transcription creates a window where multiple concurrent calls to getVideoStatus could all pass the check at line 59 (where transcriptionStatus is null), then all set PROCESSING and trigger multiple transcription attempts.

Consider using a database-level constraint (e.g., optimistic locking with a version field, or a compare-and-set operation) to ensure only one transcription is triggered. Alternatively, check the status again after the update to verify this instance "won" the race.

Copilot uses AI. Check for mistakes.

return {
transcriptionStatus: "PROCESSING",
aiGenerationStatus:
(metadata.aiGenerationStatus as AiGenerationStatus) || null,
aiTitle: metadata.aiTitle || null,
summary: metadata.summary || null,
chapters: metadata.chapters || null,
};
}

if (video.transcriptionStatus === "PROCESSING") {
const threeMinutesAgo = new Date(Date.now() - 3 * 60 * 1000);
if (video.updatedAt < threeMinutesAgo) {
Comment on lines +104 to +105
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timeout based on updatedAt will incorrectly trigger if video record is updated for unrelated reasons (e.g. metadata changes, view count). Consider using a dedicated processingStartedAt timestamp or checking transcriptionStatus update time specifically

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/web/actions/videos/get-status.ts
Line: 104-105

Comment:
timeout based on `updatedAt` will incorrectly trigger if video record is updated for unrelated reasons (e.g. metadata changes, view count). Consider using a dedicated `processingStartedAt` timestamp or checking `transcriptionStatus` update time specifically

How can I resolve this? If you propose a fix, please make it concise.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quick sanity check: does updatedAt definitely bump when you update transcriptionStatus? If it doesn’t, this timeout might never trip for a stuck PROCESSING row.

await db()
.update(videos)
.set({ transcriptionStatus: "ERROR" })
.where(eq(videos.id, videoId));

return {
transcriptionStatus: "ERROR",
aiGenerationStatus:
(metadata.aiGenerationStatus as AiGenerationStatus) || null,
aiTitle: metadata.aiTitle || null,
summary: metadata.summary || null,
chapters: metadata.chapters || null,
error: "Failed to start transcription",
error: "Transcription timed out",
};
}
Comment on lines +103 to 120
Copy link

Copilot AI Feb 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The timeout check uses video.updatedAt from the database query at the beginning of getVideoStatus, but updatedAt is automatically updated by the database via onUpdateNow() whenever the video row is modified. Between the SELECT and this timeout check, other operations (like status updates in transcribeVideoDirect) can modify the row and update the timestamp. This creates a time-of-check-to-time-of-use (TOCTOU) race condition where the timeout logic may use stale data.

Consider re-querying the video record immediately before this timeout check to get the current updatedAt value, or use a dedicated processingStartedAt timestamp field.

Copilot uses AI. Check for mistakes.
}
Expand Down
8 changes: 5 additions & 3 deletions apps/web/components/pages/HomePage/InstantModeDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,9 @@ const MockSharePage = () => {
if (!container || !video) return;

const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
(entries) => {
const entry = entries[0];
if (entry?.isIntersecting) {
if (!videoLoadedRef.current) {
video.src = "/illustrations/homepage-animation.mp4";
videoLoadedRef.current = true;
Expand All @@ -138,7 +139,8 @@ const MockSharePage = () => {
let index = 0;
const interval = setInterval(() => {
index = (index + 1) % TABS.length;
setActiveTab(TABS[index]);
const tab = TABS[index];
if (tab) setActiveTab(tab);
}, 3000);
return () => clearInterval(interval);
}, [tabInteracted]);
Expand Down
8 changes: 5 additions & 3 deletions apps/web/components/pages/HomePage/RecordingModePicker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ const RecordingModePicker = () => {
const interval = setInterval(() => {
setSelected((prev) => {
const currentIndex = modes.findIndex((m) => m.id === prev);
return modes[(currentIndex + 1) % modes.length].id;
const next = modes[(currentIndex + 1) % modes.length];
return next ? next.id : prev;
});
}, AUTO_CYCLE_INTERVAL);

Expand All @@ -113,8 +114,9 @@ const RecordingModePicker = () => {
if (!el) return;

const observer = new IntersectionObserver(
([entry]) => {
setIsInView(entry.isIntersecting);
(entries) => {
const entry = entries[0];
if (entry) setIsInView(entry.isIntersecting);
},
{ threshold: 0.3 },
);
Expand Down
6 changes: 4 additions & 2 deletions apps/web/components/pages/HomePage/ScreenshotModeDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -373,8 +373,9 @@ const MockScreenshotEditor = () => {
if (!el) return;

const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting && !isInView) {
(entries) => {
const entry = entries[0];
if (entry?.isIntersecting && !isInView) {
setIsInView(true);
}
},
Expand Down Expand Up @@ -413,6 +414,7 @@ const MockScreenshotEditor = () => {
if (cancelled) return;
const next = (current + 1) % AUTO_CONFIGS.length;
const cfg = AUTO_CONFIGS[next];
if (!cfg) return;
setGradientIndex(cfg.gradientIndex);
setPadding(cfg.padding);
setRounded(cfg.rounded);
Expand Down
1 change: 1 addition & 0 deletions apps/web/components/pages/HomePage/StudioModeDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@ const MockEditor = () => {
if (cancelled) return;
const next = (current + 1) % AUTO_CONFIGS.length;
const cfg = AUTO_CONFIGS[next];
if (!cfg) return;
setGradientIndex(cfg.gradientIndex);
setPadding(cfg.padding);
setRounded(cfg.rounded);
Expand Down
Loading
Loading