Skip to content

Conversation

@ameer2468
Copy link
Contributor

@ameer2468 ameer2468 commented Sep 9, 2025

This PR: Makes the AI metadata generator code for chapters/summary clearer and easier to work with. Also fixes go to dashboard button link.

Summary by CodeRabbit

  • New Features

    • AI metadata generation now runs in the background when enabled; the Share page updates automatically on completion.
  • Bug Fixes

    • Loading appears only during active AI processing, reducing false loading states.
    • Polling halts when AI generation is disabled, avoiding unnecessary requests.
  • UI

    • Dashboard button now opens Caps at /dashboard/caps?page=1.
  • Refactor

    • Streamlined AI processing and status checks for faster responses and more reliable AI titles, summaries, and chapters.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 9, 2025

Pre-merge checks (2 passed, 1 warning)

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title concisely and accurately reflects the two primary changes in the pull request—fixing the dashboard link and refactoring the AI metadata generator—without introducing extraneous details, ensuring clarity for reviewers scanning the change history.
Description Check ✅ Passed The description clearly summarizes the pull request’s objectives by specifying the AI metadata generator refactor for chapters and summaries alongside the dashboard link fix, directly corresponding to the changeset.

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch minor-optimization-for-intervals

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

.then(() => revalidatePath(`/s/${videoId}`))
.catch((resetError) => {
console.error(
`[Get Status] Failed to reset AI processing flag for video ${videoId}:`,

Check failure

Code scanning / CodeQL

Use of externally-controlled format string High

Format string depends on a user-provided value.

Copilot Autofix

AI 4 months ago

General Fix:
Avoid embedding untrusted values directly in formatted/log message strings. Instead, use format strings with a static message and pass untrusted data as additional parameters. This ensures identifiers like videoId are included in log output only as string values and not as part of a format string, which prevents accidental format string interpretation.

Detailed Fix:
In apps/web/actions/videos/get-status.ts on line 220, replace the template literal so that the log message uses a literal static string and passes videoId as a separate argument to console.error. For example, change

`[Get Status] Failed to reset AI processing flag for video ${videoId}:`

to:

"[Get Status] Failed to reset AI processing flag for video %s:", videoId

(if you want to use format specifiers according to Node.js' console.error conventions), or simply:

"[Get Status] Failed to reset AI processing flag for video:", videoId

and leave the error as a second (already separate) argument.

Lines to Change:

  • Edit only line 220 in apps/web/actions/videos/get-status.ts.

No Imports or Definitions Needed:
No new imports or method definitions are required since this only adjusts logging parameters.


Suggested changeset 1
apps/web/actions/videos/get-status.ts

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/apps/web/actions/videos/get-status.ts b/apps/web/actions/videos/get-status.ts
--- a/apps/web/actions/videos/get-status.ts
+++ b/apps/web/actions/videos/get-status.ts
@@ -217,9 +217,9 @@
 						.then(() => revalidatePath(`/s/${videoId}`))
 						.catch((resetError) => {
 							console.error(
-								`[Get Status] Failed to reset AI processing flag for video ${videoId}:`,
-								resetError,
+								"[Get Status] Failed to reset AI processing flag for video:", videoId, resetError,
 							);
+							
 						});
 				});
 
EOF
@@ -217,9 +217,9 @@
.then(() => revalidatePath(`/s/${videoId}`))
.catch((resetError) => {
console.error(
`[Get Status] Failed to reset AI processing flag for video ${videoId}:`,
resetError,
"[Get Status] Failed to reset AI processing flag for video:", videoId, resetError,
);

});
});

Copilot is powered by AI and may make mistakes. Always verify output.
@ameer2468 ameer2468 committed this autofix suggestion 4 months ago.
ameer2468 and others added 2 commits September 9, 2025 11:47
…olled format string

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
@ameer2468 ameer2468 merged commit 106a464 into main Sep 9, 2025
14 checks passed
@ameer2468 ameer2468 deleted the minor-optimization-for-intervals branch September 9, 2025 08:54
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
apps/web/actions/videos/generate-ai-metadata.ts (1)

81-86: Update call sites to new setAiProcessingFlag signature

Remove the stale currentMetadata argument to match the refactor above.

Apply:

-			await setAiProcessingFlag(videoId, false, metadata);
+			await setAiProcessingFlag(videoId, false);
-		if (metadata.aiProcessing) {
-			await setAiProcessingFlag(videoId, false, metadata);
+		if (metadata.aiProcessing) {
+			await setAiProcessingFlag(videoId, false);
-		if (metadata.aiProcessing) {
-			await setAiProcessingFlag(videoId, false, metadata);
+		if (metadata.aiProcessing) {
+			await setAiProcessingFlag(videoId, false);
-		await setAiProcessingFlag(videoId, true, metadata);
+		await setAiProcessingFlag(videoId, true);
-			await setAiProcessingFlag(videoId, false, metadata);
+			await setAiProcessingFlag(videoId, false);

Also applies to: 90-91, 97-98, 104-105, 293-293

🧹 Nitpick comments (8)
apps/web/actions/videos/generate-ai-metadata.ts (5)

76-86: Stale-check based on updatedAt is brittle

updatedAt changes for any row update, not just aiProcessing. Consider storing metadata.aiProcessingStartedAt (ISO string) and checking that instead to avoid false resets.


126-136: Cap transcript size to keep prompts within model context and control cost

Long VTTs can exceed context limits and increase latency/cost.

Apply:

-		const transcriptText = vtt
+		const transcriptText = vtt
 			.split("\n")
 			.filter(
 				(l) =>
 					l.trim() &&
 					l !== "WEBVTT" &&
 					!/^\d+$/.test(l.trim()) &&
 					!l.includes("-->"),
 			)
-			.join(" ");
+			.join(" ")
+			// Cap to ~50k chars (~12.5k tokens worst-case is much lower), tune as needed
+			.slice(0, 50_000);

Also applies to: 137-146


172-223: Harden validator: sort chapters and limit count

Guarantee monotonic chapter order and avoid pathological sizes.

Apply:

-					validated.chapters = validChapters.map((chapter) => ({
-						title: chapter.title.trim(),
-						start: Math.floor(chapter.start),
-					}));
+					const sanitized = validChapters
+						.map((chapter) => ({
+							title: chapter.title.trim(),
+							start: Math.floor(chapter.start),
+						}))
+						.sort((a, b) => a.start - b.start)
+						.slice(0, 100); // hard cap
+					validated.chapters = sanitized;

236-248: Trim raw-content logging to avoid noisy logs / PII bleed

On parse errors, logging the entire AI output can be large. Trim to a small preview.

Apply:

-			const parsedData = JSON.parse(cleanContent.trim());
+			const parsedData = JSON.parse(cleanContent.trim());
 			data = validateAIResponse(parsedData);
@@
-			console.error(`[generateAiMetadata] Raw content: ${content}`);
+			const preview = typeof content === "string" ? content.slice(0, 2000) : "";
+			console.error(`[generateAiMetadata] Raw content (truncated): ${preview}`);

34-48: Inspect and update JSON field without clobber
Replace the entire-metadata overwrite with a JSON path update to avoid concurrent-update data loss:

import { eq, sql } from "drizzle-orm";

async function setAiProcessingFlag(videoId: string, processing: boolean) {
  await db()
    .update(videos)
    .set({
      // Update only the aiProcessing flag in the JSON metadata
      metadata: sql`JSON_SET(COALESCE(${videos.metadata}, JSON_OBJECT()), '$.aiProcessing', ${processing})`,
    })
    .where(eq(videos.id, videoId));
}

– Only the aiProcessing field is modified, preserving any other concurrent JSON updates.
– Ensure your DB dialect supports JSON_SET; if not, fall back to a safe read-modify-write with row versioning.

apps/web/app/s/[videoId]/Share.tsx (1)

212-217: Remove or gate debug logs

Console noise in production; wrap with NODE_ENV check or remove.

Apply:

-	console.log({
-		aiLoading,
-		aiData,
-		transcriptionStatus,
-	});
+	if (process.env.NODE_ENV === "development") {
+		// eslint-disable-next-line no-console
+		console.log({ aiLoading, aiData, transcriptionStatus });
+	}
apps/web/actions/videos/get-status.ts (2)

192-225: Harden background error handling and avoid deep promise chains.

The nested .then/.catch chain is harder to follow and may drop errors; also, in some serverless runtimes, detached work can be reclaimed. Prefer an async IIFE for clarity and to ensure revalidate always runs.

Apply this refactor:

-			generateAiMetadata(videoId, video.ownerId)
-				.then(() => {
-					console.log(
-						`[Get Status] AI metadata generation completed for video ${videoId}`,
-					);
-					revalidatePath(`/s/${videoId}`);
-				})
-				.catch((error) => {
-					console.error(
-						`[Get Status] Error generating AI metadata for video ${videoId}:`,
-						error,
-					);
-					// Reset aiProcessing flag on error
-					db()
-						.select()
-						.from(videos)
-						.where(eq(videos.id, videoId))
-						.then((currentVideo) => {
-							if (currentVideo.length > 0 && currentVideo[0]) {
-								const currentMetadata =
-									(currentVideo[0].metadata as VideoMetadata) || {};
-								return db()
-									.update(videos)
-									.set({
-										metadata: {
-											...currentMetadata,
-											aiProcessing: false,
-										},
-									})
-									.where(eq(videos.id, videoId));
-							}
-						})
-						.then(() => revalidatePath(`/s/${videoId}`))
-						.catch((resetError) => {
-							console.error(
-								"[Get Status] Failed to reset AI processing flag for video:",
-								videoId,
-								resetError,
-							);
-						});
-				});
+			void (async () => {
+				try {
+					await generateAiMetadata(videoId, video.ownerId);
+					console.log(`[Get Status] AI metadata generation completed for video ${videoId}`);
+				} catch (error) {
+					console.error(`[Get Status] Error generating AI metadata for video ${videoId}:`, error);
+					try {
+						const currentVideo = await db().select().from(videos).where(eq(videos.id, videoId));
+						if (currentVideo.length > 0 && currentVideo[0]) {
+							const currentMetadata = (currentVideo[0].metadata as VideoMetadata) || {};
+							await db()
+								.update(videos)
+								.set({ metadata: { ...currentMetadata, aiProcessing: false } })
+								.where(eq(videos.id, videoId));
+						}
+					} catch (resetError) {
+						console.error("[Get Status] Failed to reset AI processing flag for video:", videoId, resetError);
+					}
+				} finally {
+					revalidatePath(`/s/${videoId}`);
+				}
+			})();

Verification: If you’re on a serverless host (e.g., Vercel), confirm that non-awaited background work spawned from a Server Action is guaranteed to finish. If not, move this to a durable job/queue or a scheduled worker and trigger it here.


173-183: Timeout logic may misfire if updatedAt isn’t bumped on metadata updates.

Your “stuck” detection uses updatedAt. The update that flips aiProcessing may not advance updatedAt unless you have a DB trigger/on-update column. If updatedAt doesn’t change, you may immediately “time out” and reset on the next poll.

Action items:

  • Verify videos.updatedAt auto-updates on UPDATEs touching metadata.
  • If not, either explicitly set updatedAt in the update, or store a metadata.aiProcessingStartedAt timestamp and use it for timeout checks.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 09110a0 and 79dd9db.

📒 Files selected for processing (4)
  • apps/web/actions/videos/generate-ai-metadata.ts (7 hunks)
  • apps/web/actions/videos/get-status.ts (2 hunks)
  • apps/web/app/s/[videoId]/Share.tsx (2 hunks)
  • apps/web/app/s/[videoId]/_components/ShareHeader.tsx (1 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
apps/web/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/**/*.{ts,tsx}: Use TanStack Query v5 for client-side server state and data fetching in the web app
Mutations should call Server Actions and perform precise cache updates with setQueryData/setQueriesData, avoiding broad invalidations
Prefer Server Components for initial data and pass initialData to client components for React Query hydration

Files:

  • apps/web/app/s/[videoId]/_components/ShareHeader.tsx
  • apps/web/app/s/[videoId]/Share.tsx
  • apps/web/actions/videos/generate-ai-metadata.ts
  • apps/web/actions/videos/get-status.ts
{apps/web,packages/ui}/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

{apps/web,packages/ui}/**/*.{ts,tsx}: Use Tailwind CSS exclusively for styling in the web app and shared React UI components
Component naming: React components in PascalCase; hooks in camelCase starting with 'use'

Files:

  • apps/web/app/s/[videoId]/_components/ShareHeader.tsx
  • apps/web/app/s/[videoId]/Share.tsx
  • apps/web/actions/videos/generate-ai-metadata.ts
  • apps/web/actions/videos/get-status.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

Use strict TypeScript and avoid any; prefer shared types from packages

**/*.{ts,tsx}: Use Biome to format/lint TypeScript with a 2-space indent
TypeScript file names should be kebab-case (e.g., user-menu.tsx)

Files:

  • apps/web/app/s/[videoId]/_components/ShareHeader.tsx
  • apps/web/app/s/[videoId]/Share.tsx
  • apps/web/actions/videos/generate-ai-metadata.ts
  • apps/web/actions/videos/get-status.ts
**/*.{tsx,jsx}

📄 CodeRabbit inference engine (AGENTS.md)

React/Solid components should be named using PascalCase

Files:

  • apps/web/app/s/[videoId]/_components/ShareHeader.tsx
  • apps/web/app/s/[videoId]/Share.tsx
apps/web/actions/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

apps/web/actions/**/*.ts: All Groq/OpenAI calls must be implemented in Next.js Server Actions under apps/web/actions
Place Server Actions for the web app under apps/web/actions and mark them with 'use server'

Files:

  • apps/web/actions/videos/generate-ai-metadata.ts
  • apps/web/actions/videos/get-status.ts
🧬 Code graph analysis (2)
apps/web/actions/videos/generate-ai-metadata.ts (5)
packages/env/server.ts (1)
  • serverEnv (80-84)
packages/database/types/metadata.ts (1)
  • VideoMetadata (8-31)
packages/database/index.ts (1)
  • db (30-35)
packages/database/schema.ts (2)
  • videos (231-277)
  • s3Buckets (362-372)
apps/web/utils/s3.ts (1)
  • createBucketProvider (372-395)
apps/web/actions/videos/get-status.ts (4)
apps/web/utils/flags.ts (2)
  • isAiGenerationEnabled (8-12)
  • FeatureFlagUser (3-6)
packages/database/index.ts (1)
  • db (30-35)
packages/database/schema.ts (1)
  • videos (231-277)
apps/web/actions/videos/generate-ai-metadata.ts (1)
  • generateAiMetadata (50-301)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
  • GitHub Check: Build Desktop (aarch64-apple-darwin, macos-latest)
  • GitHub Check: Analyze (rust)
🔇 Additional comments (7)
apps/web/app/s/[videoId]/_components/ShareHeader.tsx (1)

249-249: Dashboard link fix looks correct

Route updated to /dashboard/caps?page=1. If this path is new, ensure the server route and auth guards match the prior "/dashboard" behavior.

Would you like me to scan the repo to confirm a page exists at "apps/web/app/(org)/dashboard/caps" and accepts the "page" query?

apps/web/actions/videos/generate-ai-metadata.ts (2)

149-170: Groq→OpenAI fallback path is clean

Clear logging and fallback sequencing; no concerns.


256-263: Batch update logic looks good

Merges AI fields and clears aiProcessing; single-statement name+metadata update when renaming is appropriate.

Also applies to: 274-289

apps/web/app/s/[videoId]/Share.tsx (2)

117-123: Correctly disables polling when AI generation is off

Good alignment with the aiGenerationEnabled feature flag.


181-209: Loading-state logic is coherent and avoids flashing

Shows loading only when it adds value (AI actually processing). Nice.

apps/web/actions/videos/get-status.ts (2)

12-12: Type import addition looks good.

Importing FeatureFlagUser alongside the flag helper is appropriate for stricter typing here.


227-233: Return shape and immediate UI signal look good.

Returning aiProcessing: true with the current metadata is consistent with the background generation flow.

Comment on lines +11 to +32
async function callOpenAI(prompt: string): Promise<string> {
const aiRes = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${serverEnv().OPENAI_API_KEY}`,
},
body: JSON.stringify({
model: "gpt-4o-mini",
messages: [{ role: "user", content: prompt }],
}),
});
if (!aiRes.ok) {
const errorText = await aiRes.text();
console.error(
`[generateAiMetadata] OpenAI API error: ${aiRes.status} ${errorText}`,
);
throw new Error(`OpenAI API error: ${aiRes.status} ${errorText}`);
}
const aiJson = await aiRes.json();
return aiJson.choices?.[0]?.message?.content || "{}";
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Add timeout/abort handling to OpenAI fetch to avoid hanging requests

External calls should use timeouts; a stuck fetch will leave aiProcessing true longer than needed.

Apply:

-async function callOpenAI(prompt: string): Promise<string> {
-	const aiRes = await fetch("https://api.openai.com/v1/chat/completions", {
-		method: "POST",
-		headers: {
-			"Content-Type": "application/json",
-			Authorization: `Bearer ${serverEnv().OPENAI_API_KEY}`,
-		},
-		body: JSON.stringify({
-			model: "gpt-4o-mini",
-			messages: [{ role: "user", content: prompt }],
-		}),
-	});
-	if (!aiRes.ok) {
-		const errorText = await aiRes.text();
-		console.error(
-			`[generateAiMetadata] OpenAI API error: ${aiRes.status} ${errorText}`,
-		);
-		throw new Error(`OpenAI API error: ${aiRes.status} ${errorText}`);
-	}
-	const aiJson = await aiRes.json();
-	return aiJson.choices?.[0]?.message?.content || "{}";
-}
+async function callOpenAI(
+	prompt: string,
+	opts?: { timeoutMs?: number },
+): Promise<string> {
+	const timeoutMs = opts?.timeoutMs ?? 60_000;
+	const controller = new AbortController();
+	const timer = setTimeout(() => controller.abort(), timeoutMs);
+	try {
+		const aiRes = await fetch("https://api.openai.com/v1/chat/completions", {
+			method: "POST",
+			headers: {
+				"Content-Type": "application/json",
+				Authorization: `Bearer ${serverEnv().OPENAI_API_KEY}`,
+			},
+			body: JSON.stringify({
+				model: "gpt-4o-mini",
+				messages: [{ role: "user", content: prompt }],
+			}),
+			signal: controller.signal,
+		});
+		if (!aiRes.ok) {
+			const errorText = await aiRes.text();
+			console.error(
+				`[generateAiMetadata] OpenAI API error: ${aiRes.status} ${errorText}`,
+			);
+			throw new Error(`OpenAI API error: ${aiRes.status} ${errorText}`);
+		}
+		const aiJson = await aiRes.json();
+		return aiJson.choices?.[0]?.message?.content || "{}";
+	} catch (err: unknown) {
+		if ((err as any)?.name === "AbortError") {
+			throw new Error(`[generateAiMetadata] OpenAI request timed out after ${timeoutMs}ms`);
+		}
+		throw err;
+	} finally {
+		clearTimeout(timer);
+	}
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async function callOpenAI(prompt: string): Promise<string> {
const aiRes = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${serverEnv().OPENAI_API_KEY}`,
},
body: JSON.stringify({
model: "gpt-4o-mini",
messages: [{ role: "user", content: prompt }],
}),
});
if (!aiRes.ok) {
const errorText = await aiRes.text();
console.error(
`[generateAiMetadata] OpenAI API error: ${aiRes.status} ${errorText}`,
);
throw new Error(`OpenAI API error: ${aiRes.status} ${errorText}`);
}
const aiJson = await aiRes.json();
return aiJson.choices?.[0]?.message?.content || "{}";
}
async function callOpenAI(
prompt: string,
opts?: { timeoutMs?: number },
): Promise<string> {
const timeoutMs = opts?.timeoutMs ?? 60_000;
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeoutMs);
try {
const aiRes = await fetch("https://api.openai.com/v1/chat/completions", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${serverEnv().OPENAI_API_KEY}`,
},
body: JSON.stringify({
model: "gpt-4o-mini",
messages: [{ role: "user", content: prompt }],
}),
signal: controller.signal,
});
if (!aiRes.ok) {
const errorText = await aiRes.text();
console.error(
`[generateAiMetadata] OpenAI API error: ${aiRes.status} ${errorText}`,
);
throw new Error(`OpenAI API error: ${aiRes.status} ${errorText}`);
}
const aiJson = await aiRes.json();
return aiJson.choices?.[0]?.message?.content || "{}";
} catch (err: unknown) {
if ((err as any)?.name === "AbortError") {
throw new Error(`[generateAiMetadata] OpenAI request timed out after ${timeoutMs}ms`);
}
throw err;
} finally {
clearTimeout(timer);
}
}
🤖 Prompt for AI Agents
In apps/web/actions/videos/generate-ai-metadata.ts around lines 11 to 32, the
fetch call to OpenAI has no timeout/abort handling which can hang and leave
aiProcessing true indefinitely; wrap the fetch with an AbortController and a
timer (duration configurable via env or default, e.g. 10s), pass
controller.signal into fetch, clear the timer after fetch resolves, catch abort
errors to log a clear timeout message and throw a specific Error (or rethrow) so
upstream can clear aiProcessing, and ensure the timer is cleared in finally to
avoid leaks.

Comment on lines +106 to 113
const awsBucket = videoData.awsBucket;
if (!awsBucket) {
console.error(
`[generateAiMetadata] AWS bucket not found for video ${videoId}`,
);
throw new Error(`AWS bucket not found for video ${videoId}`);
}

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Remove hard dependency on deprecated videos.awsBucket

This blocks custom buckets/CloudFront paths even when bucketData exists. createBucketProvider(bucketData) already resolves the correct provider.

Apply:

-		const awsBucket = videoData.awsBucket;
-		if (!awsBucket) {
-			console.error(
-				`[generateAiMetadata] AWS bucket not found for video ${videoId}`,
-			);
-			throw new Error(`AWS bucket not found for video ${videoId}`);
-		}
-
 		const bucket = await createBucketProvider(bucketData);

Also applies to: 114-115

🤖 Prompt for AI Agents
In apps/web/actions/videos/generate-ai-metadata.ts around lines 106 to 115, the
code currently throws if videoData.awsBucket is missing which creates a hard
dependency on the deprecated videos.awsBucket and blocks custom
buckets/CloudFront paths even when bucketData exists; instead, remove the
awsBucket check and use createBucketProvider(bucketData) (or fall back to
bucketData or other resolved provider) so the bucket/provider is resolved from
bucketData; update the logic to call createBucketProvider(bucketData) and throw
only if neither bucketData nor a resolved provider is available.

Comment on lines +164 to 169
const isAiGenEnabled: boolean = await isAiGenerationEnabled(
videoOwnerQuery[0] as FeatureFlagUser,
);

if (videoOwnerQuery.length > 0 && videoOwnerQuery[0] && isAiGenEnabled) {
console.log(
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Guard before calling isAiGenerationEnabled to avoid a runtime crash when query returns 0 rows.

You dereference videoOwnerQuery[0] before checking length; if the user isn't found, this will pass undefined into isAiGenerationEnabled and throw at runtime.

Apply this diff to compute the flag only when a user row exists:

-		const isAiGenEnabled: boolean = await isAiGenerationEnabled(
-			videoOwnerQuery[0] as FeatureFlagUser,
-		);
-
-		if (videoOwnerQuery.length > 0 && videoOwnerQuery[0] && isAiGenEnabled) {
+		if (videoOwnerQuery.length > 0 && videoOwnerQuery[0]) {
+			const isAiGenEnabled = await isAiGenerationEnabled(
+				videoOwnerQuery[0] as FeatureFlagUser,
+			);
+			if (isAiGenEnabled) {

Note: You'll need to shift the closing braces for this nested if and keep the existing else branch intact.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const isAiGenEnabled: boolean = await isAiGenerationEnabled(
videoOwnerQuery[0] as FeatureFlagUser,
);
if (videoOwnerQuery.length > 0 && videoOwnerQuery[0] && isAiGenEnabled) {
console.log(
// Only call isAiGenerationEnabled if we actually have a video owner row
if (videoOwnerQuery.length > 0 && videoOwnerQuery[0]) {
const isAiGenEnabled = await isAiGenerationEnabled(
videoOwnerQuery[0] as FeatureFlagUser,
);
if (isAiGenEnabled) {
console.log(
🤖 Prompt for AI Agents
In apps/web/actions/videos/get-status.ts around lines 164 to 169, the code calls
isAiGenerationEnabled(videoOwnerQuery[0]) before checking that videoOwnerQuery
has at least one row; move the isAiGenerationEnabled call inside the existing if
that checks videoOwnerQuery.length > 0 && videoOwnerQuery[0] so you only compute
isAiGenEnabled when a user row exists, shifting the closing braces accordingly
to preserve the existing else branch and control flow.

Comment on lines +173 to +186
// Set aiProcessing to true immediately
await db()
.update(videos)
.set({
metadata: {
...metadata,
aiProcessing: true,
},
})
.where(eq(videos.id, videoId));

// Start AI generation asynchronously (it will skip setting aiProcessing since it's already true)
generateAiMetadata(videoId, video.ownerId)
.then(() => {
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Logic bug: Pre-setting aiProcessing causes generateAiMetadata to early-return (no AI work starts).

generateAiMetadata currently returns early when metadata.aiProcessing === true and updatedAt is recent (per its documented behavior). Because you set aiProcessing=true first in this action, the subsequent call likely does nothing.

Two safe fixes (pick one):

  • Option A (simplest here): Let generateAiMetadata claim the job itself; do not set aiProcessing in this action.
-			// Set aiProcessing to true immediately
-			await db()
-				.update(videos)
-				.set({
-					metadata: {
-						...metadata,
-						aiProcessing: true,
-					},
-				})
-				.where(eq(videos.id, videoId));
-
-			// Start AI generation asynchronously (it will skip setting aiProcessing since it's already true)
-			generateAiMetadata(videoId, video.ownerId)
+			// Start AI generation asynchronously; it will set aiProcessing itself.
+			void generateAiMetadata(videoId, video.ownerId)
 				.then(() => {
  • Option B (keep “early UI flip”): Keep your DB flip, but add an override to generateAiMetadata so it doesn’t bail when aiProcessing is already true. For example:

In this file:

-			generateAiMetadata(videoId, video.ownerId)
+			void generateAiMetadata(videoId, video.ownerId, { assumeClaimed: true })

And in generate-ai-metadata.ts (outline):

export async function generateAiMetadata(
  videoId: string,
  userId: string,
  opts?: { assumeClaimed?: boolean }
) {
  // ...
  if (metadata.aiProcessing && !opts?.assumeClaimed) {
    // existing early-return logic
    return;
  }
  // proceed with generation
}

Follow-up: If you keep Option A, consider adding a compare-and-set (atomic claim) in generateAiMetadata to avoid double work under concurrent triggers.

🤖 Prompt for AI Agents
In apps/web/actions/videos/get-status.ts around lines 173–186, the code pre-sets
metadata.aiProcessing = true before calling generateAiMetadata which causes
generateAiMetadata to early-return (it bails when aiProcessing is already true);
fix by either removing the DB update here and letting generateAiMetadata claim
the job itself (Option A), or keep the UI flip but change the generateAiMetadata
API to accept an opts flag (e.g., assumeClaimed) and call
generateAiMetadata(videoId, video.ownerId, { assumeClaimed: true }) so it does
not bail when aiProcessing is already true (Option B); if choosing Option A,
ensure generateAiMetadata performs an atomic compare-and-set claim to avoid
double work under concurrency.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants