{"id":38076,"date":"2025-08-04T07:00:00","date_gmt":"2025-08-04T14:00:00","guid":{"rendered":"https:\/\/cloudinary.com\/blog\/?p=38076"},"modified":"2025-07-31T11:35:31","modified_gmt":"2025-07-31T18:35:31","slug":"ai-video-insights-app-next-js-cloudinary-openai-prisma","status":"publish","type":"post","link":"https:\/\/cloudinary.com\/blog\/ai-video-insights-app-next-js-cloudinary-openai-prisma","title":{"rendered":"Building an AI Video Insights App With Next.js, Cloudinary, OpenAI, and Prisma"},"content":{"rendered":"<div class=\"wp-block-cloudinary-markdown \"><p><a href=\"https:\/\/github.com\/musebe\/ai-video-insights\"><strong>GitHub Repository<\/strong><\/a> | <a href=\"https:\/\/ai-video-insights.vercel.app\/\"><strong>Live Demo<\/strong><\/a><\/p>\n<p>Like many other organizations, your library is filled with valuable content: user interviews, team meetings, marketing webinars, online courses \u2013 the list goes on. The information you need to pull from is buried in hours of video footage, making them difficult to access, search, or repurpose. And it could take days to sift through all your content.<\/p>\n<p>What if you could instantly get a concise summary of any video, ask specific questions about its content (\u201cWhat were the key results of the Q3 campaign?\u201d), and even generate promotional social media posts with a single click? That\u2019s the power of turning raw video into actionable, conversational intelligence.<\/p>\n<p>This is exactly what we\u2019ll build in this guide: A full-stack Next.js application that transforms your video library into an interactive knowledge base. We\u2019ll show you, step-by-step, how to combine the power of three best-in-class technologies:<\/p>\n<ul>\n<li>\n<p><strong>Cloudinary.<\/strong> To handle our entire video processing pipeline, from robust, large-file uploads to automated transcription and subtitle generation.<\/p>\n<\/li>\n<li>\n<p><strong>OpenAI.<\/strong> To provide the language intelligence needed to summarize transcripts, create marketing copy, and power a conversational chat experience.<\/p>\n<\/li>\n<li>\n<p><strong>Next.js.<\/strong> To serve as the modern, high-performance framework that ties everything together into a sleek, responsive user interface.<\/p>\n<\/li>\n<\/ul>\n<p>By the end of this tutorial, you\u2019ll have a powerful, scalable application and a clear understanding of how to compose modern APIs to build sophisticated AI-powered features. Let\u2019s get started.<\/p>\n<h2>Project Setup: From Starter Branch to Database Ready<\/h2>\n<p>Clone a starter template with Next.js 15, install the database tool, and define the data structure.<\/p>\n<h3>Clone the Starter Branch<\/h3>\n<p>Includes App Router, Tailwind CSS, and Shadcn UI.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php shcb-wrap-lines\">git  <span class=\"hljs-keyword\">clone<\/span>  https:<span class=\"hljs-comment\">\/\/github.com\/musebe\/ai-video-insights.git<\/span>\n\ncd  ai-video-insights\n\ngit  checkout  starter\n\nnpm  install\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<h3>Set Up Environment Variables<\/h3>\n<p>Create a <code>.env<\/code> file in the root for secrets and config. Prisma and Next.js will read from it.<\/p>\n<pre class=\"js-syntax-highlighted\"><code># Database\nDATABASE_URL=&quot;your_postgresql_direct_connection_string&quot;\n\n# Cloudinary (Server)\nCLOUDINARY_CLOUD_NAME=&quot;your_cloud_name&quot;\nCLOUDINARY_API_KEY=&quot;your_api_key&quot;\nCLOUDINARY_API_SECRET=&quot;your_api_secret&quot;\n\n# Cloudinary (Client)\nNEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=&quot;your_cloud_name&quot;\nNEXT_PUBLIC_APP_URL=&quot;http:\/\/localhost:3000&quot;\n\n# OpenAI\nOPENAI_API_KEY=&quot;sk-...&quot;\n<\/code><\/pre>\n<p>Get credentials from Cloudinary, OpenAI, and your PostgreSQL provider.<\/p>\n<h3>Initialize Prisma and Define Schema<\/h3>\n<p>Install Prisma and set up tables for folders and videos.<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">npm  install  prisma  --save-dev\nnpx  prisma  init  --datasource-provider  postgresql\n<\/code><\/span><\/pre>\n<p>Replace <code>prisma\/schema.prisma<\/code> with:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\">model Folder {\n  id     <span class=\"hljs-built_in\">String<\/span>  @id @<span class=\"hljs-keyword\">default<\/span>(cuid())\n  name   <span class=\"hljs-built_in\">String<\/span>\n  videos Video&#91;]\n}\n\nmodel Video {\n  id                 <span class=\"hljs-built_in\">String<\/span>   @id @<span class=\"hljs-keyword\">default<\/span>(cuid())\n  title              <span class=\"hljs-built_in\">String<\/span>\n  cloudinaryPublicId <span class=\"hljs-built_in\">String<\/span>   @unique\n  cloudinaryUrl      <span class=\"hljs-built_in\">String<\/span>\n  transcript         <span class=\"hljs-built_in\">String<\/span>?  @db.Text\n  summary            <span class=\"hljs-built_in\">String<\/span>?  @db.Text\n  status             <span class=\"hljs-built_in\">String<\/span>   @<span class=\"hljs-keyword\">default<\/span>(<span class=\"hljs-string\">\"PROCESSING\"<\/span>)\n  srtUrl             <span class=\"hljs-built_in\">String<\/span>?\n  vttUrl             <span class=\"hljs-built_in\">String<\/span>?\n  subtitledUrl       <span class=\"hljs-built_in\">String<\/span>?\n  folder             Folder   @relation(fields: &#91;folderId], <span class=\"hljs-attr\">references<\/span>: &#91;id])\n  folderId           <span class=\"hljs-built_in\">String<\/span>\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<blockquote>\n<p><a href=\"https:\/\/github.com\/musebe\/ai-video-insights\/blob\/main\/prisma\/schema.prisma\">View the full schema on GitHub<\/a>.<\/p>\n<\/blockquote>\n<h3>Sync the Database<\/h3>\n<p>Push the schema to your PostgreSQL DB:<\/p>\n<pre class=\"js-syntax-highlighted\"><span><code class=\"hljs shcb-wrap-lines\">npx  prisma  db  push\n<\/code><\/span><\/pre>\n<p>Project setup is complete. Ready for Cloudinary automation.<\/p>\n<h2>The Automation Engine: Configure a \u2018Smart\u2019 Cloudinary Upload Preset<\/h2>\n<p>The foundation of our application is not just storing videos, but processing them intelligently. Instead of building a complex, server-side processing queue, we can offload the entire workflow to a <strong>Cloudinary Upload Preset<\/strong>. This is the most critical piece of configuration in our project.<\/p>\n<p>An Upload Preset is a collection of instructions that Cloudinary applies to every file uploaded with it. It allows us to define a complex chain of actions that run automatically, turning a simple upload into a powerful processing pipeline.<\/p>\n<p>Here\u2019s how to create our <code>ai_video_final<\/code> preset for a fully automated, \u201cfire-and-forget\u201d workflow.<\/p>\n<h3>Step-by-Step Configuration<\/h3>\n<ol>\n<li>\n<p>Navigate to your <a href=\"https:\/\/cloudinary.com\/console\/settings\/upload\">Cloudinary Settings &gt; Upload<\/a>.<\/p>\n<\/li>\n<li>\n<p>Scroll down to <strong>Upload Presets<\/strong> and click <strong>Add upload preset<\/strong>.<\/p>\n<\/li>\n<li>\n<p>Configure the following tabs:<\/p>\n<\/li>\n<\/ol>\n<ul>\n<li>\n<p><strong>General Tab:<\/strong><\/p>\n<\/li>\n<li>\n<p><strong>Preset name:<\/strong>  <code>ai_video_final<\/code><\/p>\n<\/li>\n<li>\n<p><strong>Signing mode:<\/strong>  <code>Unsigned<\/code>. This is crucial. It allows our Next.js frontend to upload files directly to Cloudinary from the user\u2019s browser, bypassing our server and its file size limits.<\/p>\n<\/li>\n<li>\n<p><strong>Add-ons Tab:<\/strong><\/p>\n<\/li>\n<li>\n<p>Find the <strong>Microsoft Azure Video Indexer<\/strong> (or your preferred transcription engine).<\/p>\n<\/li>\n<li>\n<p>Click <strong>+ Add to Preset<\/strong>.<\/p>\n<\/li>\n<li>\n<p>In the modal, ensure both <strong>Generate standard subtitle format (SRT)<\/strong> and <strong>Generate standard subtitle format (VTT)<\/strong> are toggled <strong>ON<\/strong>.<\/p>\n<\/li>\n<li>\n<p><strong>Manage and Analyze Tab:<\/strong><\/p>\n<\/li>\n<li>\n<p>Toggle <strong>Auto transcription \u25b6 Video<\/strong> to <strong>ON<\/strong>. This tells Cloudinary to also create a <code>.transcript<\/code> file, which is a clean JSON representation of the transcript that\u2019s easy for our application to parse.<\/p>\n<\/li>\n<li>\n<p><strong>Advanced Tab:<\/strong><\/p>\n<\/li>\n<li>\n<p><strong>Notification URL:<\/strong> This is the magic that connects Cloudinary back to our application. Enter your public webhook URL here (e.g., <code>https:\/\/&lt;your-ngrok-url&gt;\/api\/cloudinary\/webhook<\/code>).<\/p>\n<\/li>\n<\/ul>\n<ol start=\"4\">\n<li>Click the main <strong>Save<\/strong> button at the top right.<\/li>\n<\/ol>\n<p>With this single preset, every video upload will now automatically trigger a multi-step transcription process, and our application will be notified the moment the results are ready.<\/p>\n<h3>The User\u2019s Gateway: A Robust Client-Side Upload Experience<\/h3>\n<p>With our powerful Upload Preset handling the complex processing, the frontend\u2019s job becomes much simpler. Our goal is to provide a smooth, reliable way for users to get their video files into the system.<\/p>\n<p>The biggest challenge with video is file size. A standard serverless function has a small request body limit (often just a few megabytes), which is not nearly enough for a video file. Sending the file to our own server first is not an option.<\/p>\n<p>The solution is to upload the file <strong>directly from the user\u2019s browser to Cloudinary<\/strong>, bypassing our server entirely. The best tool for this job is the official <strong>Cloudinary Upload Widget<\/strong>. It automatically handles large files by breaking them into manageable chunks, shows a detailed progress bar, and manages network errors and retries.<\/p>\n<h2>Build the Upload Component<\/h2>\n<p>Our entire upload experience is handled by a single React component: <code>VideoUploadArea.tsx<\/code>.<\/p>\n<p>The core logic is in the <code>openUploadWidget<\/code> function. When the user clicks the \u201cOpen Upload Widget\u201d button, we initialize the widget with our configuration. The most important parameters are <code>cloudName<\/code> and <code>uploadPreset<\/code>, which tell the widget which account and which set of instructions to use.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-comment\">\/\/ src\/components\/video\/VideoUploadArea.tsx<\/span>\n\n<span class=\"hljs-keyword\">const<\/span> widget = <span class=\"hljs-built_in\">window<\/span>.cloudinary.createUploadWidget(\n  {\n    <span class=\"hljs-attr\">cloudName<\/span>: process.env.NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME,\n    <span class=\"hljs-attr\">uploadPreset<\/span>: <span class=\"hljs-string\">\"ai_video_final\"<\/span>, <span class=\"hljs-comment\">\/\/ Our powerful unsigned preset<\/span>\n    <span class=\"hljs-attr\">folder<\/span>: <span class=\"hljs-string\">`ai-videos\/<span class=\"hljs-subst\">${folderName}<\/span>`<\/span>, <span class=\"hljs-comment\">\/\/ Dynamically set the folder<\/span>\n    <span class=\"hljs-attr\">sources<\/span>: &#91;<span class=\"hljs-string\">\"local\"<\/span>, <span class=\"hljs-string\">\"camera\"<\/span>],\n    <span class=\"hljs-attr\">multiple<\/span>: <span class=\"hljs-literal\">false<\/span>,\n  },\n  (error, result) =&gt; {\n    <span class=\"hljs-keyword\">if<\/span> (result &amp;&amp; result.event === <span class=\"hljs-string\">\"success\"<\/span>) {\n      <span class=\"hljs-comment\">\/\/ This is where the magic happens.<\/span>\n      <span class=\"hljs-comment\">\/\/ After a successful upload, we get all the video info back.<\/span>\n      <span class=\"hljs-keyword\">const<\/span> info = result.info;\n\n      <span class=\"hljs-comment\">\/\/ We construct the necessary URLs...<\/span>\n      <span class=\"hljs-keyword\">const<\/span> srtUrl = buildSrtUrl(info);\n      <span class=\"hljs-keyword\">const<\/span> vttUrl = buildVttUrl(info);\n\n      <span class=\"hljs-comment\">\/\/ ...and save everything to our database.<\/span>\n      saveVideoMutation.mutate({\n        <span class=\"hljs-attr\">title<\/span>: info.original_filename,\n        <span class=\"hljs-attr\">cloudinaryPublicId<\/span>: info.public_id,\n        <span class=\"hljs-comment\">\/\/ ... and so on<\/span>\n      });\n    }\n  }\n);\n\nwidget.open();\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>After the upload to Cloudinary is complete, the widget\u2019s callback function gives us a <code>result<\/code> object containing all the URLs and metadata for the newly uploaded video. We then immediately call our <code>saveVideoMutation<\/code> to send this clean, structured data to our <code>\/api\/videos<\/code> route, creating the initial record in our database.<\/p>\n<blockquote>\n<p>View the full component on <a href=\"https:\/\/github.com\/musebe\/ai-video-insights\/blob\/main\/src\/components\/video\/VideoUploadArea.tsx\">GitHub<\/a>.<\/p>\n<\/blockquote>\n<h3>Closing the Loop: Handle Asynchronous Jobs With a Webhook<\/h3>\n<p>Our application now has a robust upload process, but there\u2019s a missing piece. Transcription is an asynchronous job; it can take several minutes to complete after the initial upload is finished. How does our application know when the transcript is ready?<\/p>\n<p>We\u2019d ask Cloudinary, \u201cAre you done yet?\u201d but that\u2019s inefficient. A better solution is to use a <strong>webhook<\/strong>. This is a simple API route in our application that acts as a \u201clistener.\u201d We\u2019ve already configured our Cloudinary Upload Preset to send a notification to this URL the moment the transcription process is complete.<\/p>\n<p>This event-driven architecture is incredibly scalable and efficient.<\/p>\n<h3>Build the Webhook Listener<\/h3>\n<p>Our webhook is a single <code>POST<\/code> endpoint located at <code>\/app\/api\/cloudinary\/webhook\/route.ts<\/code>. Its job is to securely receive the notification, verify it came from Cloudinary, and update our database with the final, complete data.<\/p>\n<p>The core logic is straightforward:<\/p>\n<ol>\n<li>\n<p>Receive the notification and check that the <code>info_status<\/code> is <code>complete<\/code>.<\/p>\n<\/li>\n<li>\n<p>Use the <code>public_id<\/code> from the notification to make a fresh API call back to Cloudinary, requesting the full details of the video, including the URLs of the newly generated <code>.srt<\/code> and <code>.vtt<\/code> files.<\/p>\n<\/li>\n<li>\n<p>Fetch the raw text from the <code>.transcript<\/code> file.<\/p>\n<\/li>\n<li>\n<p>Update the video\u2019s record in our Prisma database with the transcript text and correct subtitle URLs, and finally, set its <code>status<\/code> to <code>COMPLETED<\/code>.<\/p>\n<\/li>\n<\/ol>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-comment\">\/\/ src\/app\/api\/cloudinary\/webhook\/route.ts<\/span>\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-keyword\">async<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">POST<\/span>(<span class=\"hljs-params\">request: Request<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">try<\/span> {\n    <span class=\"hljs-keyword\">const<\/span> body = <span class=\"hljs-keyword\">await<\/span> request.json();\n\n    <span class=\"hljs-comment\">\/\/ Check if the notification is for a completed transcription<\/span>\n    <span class=\"hljs-keyword\">if<\/span> (\n      body.info_kind === <span class=\"hljs-string\">\"auto_transcription\"<\/span> &amp;&amp;\n      body.info_status === <span class=\"hljs-string\">\"complete\"<\/span>\n    ) {\n      <span class=\"hljs-keyword\">const<\/span> publicId = body.public_id;\n\n      <span class=\"hljs-comment\">\/\/ 1. Get the full, updated details from Cloudinary<\/span>\n      <span class=\"hljs-keyword\">const<\/span> resourceDetails = <span class=\"hljs-keyword\">await<\/span> cloudinary.api.resource(publicId, {\n        <span class=\"hljs-attr\">resource_type<\/span>: <span class=\"hljs-string\">\"video\"<\/span>,\n        <span class=\"hljs-attr\">derived<\/span>: <span class=\"hljs-literal\">true<\/span>, <span class=\"hljs-comment\">\/\/ This is the key to getting SRT\/VTT info<\/span>\n      });\n\n      <span class=\"hljs-comment\">\/\/ 2. Fetch the raw transcript text<\/span>\n      <span class=\"hljs-keyword\">const<\/span> fullTranscript = <span class=\"hljs-keyword\">await<\/span> fetchTranscript(publicId);\n\n      <span class=\"hljs-comment\">\/\/ 3. Update our database with the final data<\/span>\n      <span class=\"hljs-keyword\">await<\/span> prisma.video.update({\n        <span class=\"hljs-attr\">where<\/span>: { <span class=\"hljs-attr\">cloudinaryPublicId<\/span>: publicId },\n        <span class=\"hljs-attr\">data<\/span>: {\n          <span class=\"hljs-attr\">transcript<\/span>: fullTranscript,\n          <span class=\"hljs-attr\">status<\/span>: <span class=\"hljs-string\">\"COMPLETED\"<\/span>,\n          <span class=\"hljs-attr\">srtUrl<\/span>: srtUrl, <span class=\"hljs-comment\">\/\/ The correct URL from resourceDetails<\/span>\n          <span class=\"hljs-attr\">vttUrl<\/span>: vttUrl, <span class=\"hljs-comment\">\/\/ The correct URL from resourceDetails<\/span>\n        },\n      });\n    }\n    <span class=\"hljs-keyword\">return<\/span> <span class=\"hljs-keyword\">new<\/span> NextResponse(<span class=\"hljs-string\">\"Webhook received\"<\/span>, { <span class=\"hljs-attr\">status<\/span>: <span class=\"hljs-number\">200<\/span> });\n  } <span class=\"hljs-keyword\">catch<\/span> (error) {\n    <span class=\"hljs-comment\">\/\/ ... error handling<\/span>\n  }\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>This webhook is the final step in our data pipeline. Once it runs, the video is fully processed, and all the AI features in our UI will automatically unlock.<\/p>\n<blockquote>\n<p>View the full webhook on <a href=\"https:\/\/github.com\/musebe\/ai-video-insights\/blob\/main\/src\/app\/api\/cloudinary\/webhook\/route.ts\">GitHub<\/a>.<\/p>\n<\/blockquote>\n<h3>Generate Intelligence: Create Summaries and Social Posts With OpenAI<\/h3>\n<p>With a clean transcript saved in our database, we can now leverage the power of Large Language Models (LLMs) to understand and repurpose the video\u2019s content. We\u2019ll use the OpenAI API to generate concise summaries and promotional social media posts.<\/p>\n<p>The key to getting high-quality results from an LLM is <strong>prompt engineering<\/strong>. This involves giving the model a clear, specific set of instructions.<\/p>\n<h2>Build the Summarization API<\/h2>\n<p>Our first AI feature is a \u201cGenerate Summary\u201d button that appears once a transcript is ready. Clicking it calls our <code>\/api\/openai\/summarize<\/code> route. This route fetches the video\u2019s transcript from our database and sends it to OpenAI with a carefully crafted prompt.<\/p>\n<p>The core of this API route is the OpenAI API call:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-comment\">\/\/ src\/app\/api\/openai\/summarize\/route.ts<\/span>\n\n<span class=\"hljs-comment\">\/\/ 1. Fetch the transcript from our database<\/span>\n<span class=\"hljs-keyword\">const<\/span> video = <span class=\"hljs-keyword\">await<\/span> prisma.video.findUnique({ <span class=\"hljs-attr\">where<\/span>: { <span class=\"hljs-attr\">id<\/span>: videoId } });\n<span class=\"hljs-keyword\">if<\/span> (!video || !video.transcript) {\n  <span class=\"hljs-comment\">\/* ...handle error... *\/<\/span>\n}\n\n<span class=\"hljs-comment\">\/\/ 2. Send the transcript to OpenAI with a specific prompt<\/span>\n<span class=\"hljs-keyword\">const<\/span> response = <span class=\"hljs-keyword\">await<\/span> openai.chat.completions.create({\n  <span class=\"hljs-attr\">model<\/span>: <span class=\"hljs-string\">\"gpt-3.5-turbo\"<\/span>,\n  <span class=\"hljs-attr\">messages<\/span>: &#91;\n    {\n      <span class=\"hljs-attr\">role<\/span>: <span class=\"hljs-string\">\"system\"<\/span>,\n      <span class=\"hljs-attr\">content<\/span>:\n        <span class=\"hljs-string\">\"You are a helpful assistant designed to summarize video transcripts concisely. Provide a summary in about 3-4 sentences.\"<\/span>,\n    },\n    {\n      <span class=\"hljs-attr\">role<\/span>: <span class=\"hljs-string\">\"user\"<\/span>,\n      <span class=\"hljs-attr\">content<\/span>: <span class=\"hljs-string\">`Please summarize the following transcript:\\n\\n<span class=\"hljs-subst\">${video.transcript}<\/span>`<\/span>,\n    },\n  ],\n});\n\n<span class=\"hljs-keyword\">const<\/span> summary = response.choices&#91;<span class=\"hljs-number\">0<\/span>].message.content;\n\n<span class=\"hljs-comment\">\/\/ 3. Save the generated summary back to our database<\/span>\n<span class=\"hljs-keyword\">await<\/span> prisma.video.update({\n  <span class=\"hljs-attr\">where<\/span>: { <span class=\"hljs-attr\">id<\/span>: videoId },\n  <span class=\"hljs-attr\">data<\/span>: { <span class=\"hljs-attr\">summary<\/span>: summary.trim() },\n});\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>By defining a clear <code>system<\/code> role and giving a direct command, we ensure the AI returns a consistently formatted summary every time.<\/p>\n<blockquote>\n<p>View the full API route on <a href=\"https:\/\/github.com\/musebe\/ai-video-insights\/blob\/main\/src\/app\/api\/openai\/summarize\/route.ts\">GitHub<\/a>.<\/p>\n<\/blockquote>\n<h2>Generate Social Posts<\/h2>\n<p>We use the exact same pattern to generate social media posts. The only difference is the prompt. A \u201cGenerate Post\u201d dropdown in the UI calls our <code>\/api\/openai\/social-post<\/code> route, passing along the desired platform (e.g., \u2018linkedin\u2019).<\/p>\n<p>The API route then selects a platform-specific prompt to get a tailored result:<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"PHP\" data-shcb-language-slug=\"php\"><span><code class=\"hljs language-php shcb-wrap-lines\"><span class=\"hljs-comment\">\/\/ src\/app\/api\/openai\/social-post\/route.ts<\/span>\n\n<span class=\"hljs-comment\">\/\/ Example prompt for LinkedIn<\/span>\n<span class=\"hljs-keyword\">const<\/span> prompt = `Create a professional LinkedIn post to promote a <span class=\"hljs-keyword\">new<\/span> video. The post should be engaging, informative, <span class=\"hljs-keyword\">and<\/span> <span class=\"hljs-keyword\">include<\/span> <span class=\"hljs-number\">3<\/span><span class=\"hljs-number\">-5<\/span> relevant business hashtags. The post is based on the following summary: <span class=\"hljs-string\">\"${video.summary}\"<\/span>`;\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">PHP<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">php<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>This simple but powerful pattern allows us to add a wide variety of AI-driven content generation features to our application.<\/p>\n<h3>The Interactive Experience (Part 1): AI Chat<\/h3>\n<p>Generating summaries is powerful, but true intelligence comes from conversation. We want users to be able to ask specific, follow-up questions about their video\u2019s content. To build this, we need a real-time, streaming chat interface.<\/p>\n<p>The <strong>Vercel AI SDK<\/strong> is the perfect tool for this. It provides a client-side React hook, <code>useChat<\/code>, that handles all the complex state management for us: tracking the conversation history, managing the user\u2019s input, and updating the UI instantly as the AI\u2019s response streams in.<\/p>\n<h3>Implement the Chat on the Frontend<\/h3>\n<p>Integrating the chat into our <code>InsightsPanel.tsx<\/code> component is remarkably simple. We just need to call the <code>useChat<\/code> hook and connect its state to our UI elements.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-comment\">\/\/ src\/components\/insights\/InsightsPanel.tsx<\/span>\n\n<span class=\"hljs-keyword\">import<\/span> { useChat } <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">\"ai\/react\"<\/span>;\n\n\n<span class=\"hljs-keyword\">export<\/span> <span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">InsightsPanel<\/span>(<span class=\"hljs-params\">{ video }: InsightsPanelProps<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> { messages, input, handleInputChange, handleSubmit, isLoading } =\n    useChat({\n      <span class=\"hljs-comment\">\/\/ 1. Point to our chat API route<\/span>\n      <span class=\"hljs-attr\">api<\/span>: <span class=\"hljs-string\">\"\/api\/openai\/chat\"<\/span>,\n      <span class=\"hljs-comment\">\/\/ 2. Send the videoId with every request<\/span>\n      <span class=\"hljs-attr\">body<\/span>: {\n        <span class=\"hljs-attr\">videoId<\/span>: video.id,\n      },\n      <span class=\"hljs-comment\">\/\/ 3. Pre-populate the chat with the summary if it exists<\/span>\n      <span class=\"hljs-attr\">initialMessages<\/span>: video.summary\n        ? &#91;\n            {\n              <span class=\"hljs-attr\">id<\/span>: <span class=\"hljs-string\">\"initial-summary\"<\/span>,\n              <span class=\"hljs-attr\">role<\/span>: <span class=\"hljs-string\">\"assistant\"<\/span>,\n              <span class=\"hljs-attr\">content<\/span>: <span class=\"hljs-string\">`**Summary:**\\n<span class=\"hljs-subst\">${video.summary}<\/span>`<\/span>,\n            },\n          ]\n        : &#91;],\n    });\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>This single hook gives us everything we need to build a fully functional chat interface.<\/p>\n<h3>Power the Chat on the Backend<\/h3>\n<p>The <code>useChat<\/code> hook sends its data to our <code>\/api\/openai\/chat<\/code> route. This is where we perform the most important step: grounding the AI.<\/p>\n<p>To prevent the AI from making up information, we construct a special <strong>system prompt<\/strong>. This prompt provides the full transcript to the AI and gives it a strict set of rules: \u201cYou are an expert assistant for this video. Your task is to answer questions based ONLY on the provided transcript.\u201d<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-comment\">\/\/ src\/app\/api\/openai\/chat\/route.ts<\/span>\n\n<span class=\"hljs-comment\">\/\/ 1. Fetch the transcript from our database<\/span>\n<span class=\"hljs-keyword\">const<\/span> video = <span class=\"hljs-keyword\">await<\/span> prisma.video.findUnique({ <span class=\"hljs-attr\">where<\/span>: { <span class=\"hljs-attr\">id<\/span>: videoId } });\n<span class=\"hljs-keyword\">if<\/span> (!video || !video.transcript) {\n  <span class=\"hljs-comment\">\/* ...handle error... *\/<\/span>\n}\n\n<span class=\"hljs-comment\">\/\/ 2. Construct the system prompt with the full transcript<\/span>\n<span class=\"hljs-keyword\">const<\/span> systemPrompt = <span class=\"hljs-string\">`You are an expert AI assistant for the video titled \"<span class=\"hljs-subst\">${video.title}<\/span>\".\n      Your task is to answer questions based ONLY on the provided transcript...\n      Here is the full transcript for your reference:\n      ---\n      <span class=\"hljs-subst\">${video.transcript}<\/span>\n      ---`<\/span>;\n\n<span class=\"hljs-comment\">\/\/ 3. Send the system prompt and user messages to OpenAI<\/span>\n<span class=\"hljs-keyword\">const<\/span> result = <span class=\"hljs-keyword\">await<\/span> streamText({\n  <span class=\"hljs-attr\">model<\/span>: openaiProvider(<span class=\"hljs-string\">\"gpt-4-turbo\"<\/span>),\n  <span class=\"hljs-attr\">system<\/span>: systemPrompt,\n  messages, <span class=\"hljs-comment\">\/\/ The user's conversation history<\/span>\n});\n\n<span class=\"hljs-comment\">\/\/ 4. Stream the response back to the client<\/span>\n<span class=\"hljs-keyword\">return<\/span> result.toDataStreamResponse();\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<p>By combining the simple <code>useChat<\/code> hook on the frontend with a carefully grounded prompt on the backend, we create a powerful and reliable conversational interface for any video.<\/p>\n<blockquote>\n<p>View the full component on <a href=\"https:\/\/github.com\/musebe\/ai-video-insights\/blob\/main\/src\/components\/insights\/InsightsPanel.tsx\">GitHub<\/a>.<\/p>\n<\/blockquote>\n<h2>The Interactive Experience (Part 2): The Transcript Editor<\/h2>\n<p>AI transcription is powerful, but it\u2019s not always perfect. Names, technical jargon, or unclear audio can lead to errors. To make our application truly professional, we need to give users the ability to review and correct the generated transcript.<\/p>\n<p>We built a dedicated \u201cEditor\u201d tab that transforms the raw VTT subtitle file into a user-friendly, editable format. This creates a complete feedback loop: we use AI to get a first draft, and then empower the user to achieve 100% accuracy.<\/p>\n<h3>Load and Parse the Transcript<\/h3>\n<p>When a user clicks the \u201cLoad Editable Transcript\u201d button, we trigger our <code>\/api\/transcript<\/code> route. This endpoint\u2019s job is to:<\/p>\n<ol>\n<li>\n<p>Fetch the <code>.vtt<\/code> file from the URL stored in our database.<\/p>\n<\/li>\n<li>\n<p>Parse the raw VTT content into a structured JSON array, where each object contains a <code>timestamp<\/code> and the corresponding <code>text<\/code>.<\/p>\n<\/li>\n<li>\n<p>Save this clean JSON array back to the <code>transcript<\/code> column in our database, overwriting the old plain text version.<\/p>\n<\/li>\n<\/ol>\n<p>The core of this process is a simple but effective parsing function that reads the VTT file line by line.<\/p>\n<pre class=\"js-syntax-highlighted\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript shcb-wrap-lines\"><span class=\"hljs-comment\">\/\/ src\/app\/api\/transcript\/route.ts<\/span>\n\n<span class=\"hljs-comment\">\/\/ A simple VTT parser<\/span>\n<span class=\"hljs-function\"><span class=\"hljs-keyword\">function<\/span> <span class=\"hljs-title\">parseVTT<\/span>(<span class=\"hljs-params\">vttContent: string<\/span>) <\/span>{\n  <span class=\"hljs-keyword\">const<\/span> lines = vttContent.split(<span class=\"hljs-string\">\"\\n\"<\/span>);\n  <span class=\"hljs-keyword\">const<\/span> cues = &#91;];\n  <span class=\"hljs-keyword\">let<\/span> currentCue = <span class=\"hljs-literal\">null<\/span>;\n\n  <span class=\"hljs-keyword\">for<\/span> (<span class=\"hljs-keyword\">const<\/span> line <span class=\"hljs-keyword\">of<\/span> lines) {\n    <span class=\"hljs-keyword\">if<\/span> (line.includes(<span class=\"hljs-string\">\"--&gt;\"<\/span>)) {\n      <span class=\"hljs-comment\">\/\/ This line is a timestamp<\/span>\n      <span class=\"hljs-keyword\">if<\/span> (currentCue) cues.push(currentCue);\n      currentCue = { <span class=\"hljs-attr\">timestamp<\/span>: line.trim(), <span class=\"hljs-attr\">text<\/span>: <span class=\"hljs-string\">\"\"<\/span> };\n    } <span class=\"hljs-keyword\">else<\/span> <span class=\"hljs-keyword\">if<\/span> (currentCue &amp;&amp; line.trim() !== <span class=\"hljs-string\">\"\"<\/span> &amp;&amp; !<span class=\"hljs-regexp\">\/^\\d+$\/<\/span>.test(line.trim())) {\n      <span class=\"hljs-comment\">\/\/ This is the text content for the current timestamp<\/span>\n      currentCue.text += (currentCue.text ? <span class=\"hljs-string\">\"\\n\"<\/span> : <span class=\"hljs-string\">\"\"<\/span>) + line.trim();\n    }\n  }\n  <span class=\"hljs-keyword\">if<\/span> (currentCue) cues.push(currentCue);\n  <span class=\"hljs-keyword\">return<\/span> cues;\n}\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n<h3>Edit and Save Changes<\/h3>\n<p>Once the JSON is saved, our UI automatically renders it as a list of text boxes, each paired with its timestamp. When the user edits the text and clicks <strong>Save Changes<\/strong>, we trigger our <code>\/api\/transcript\/update<\/code> route. This route:<\/p>\n<ol>\n<li>\n<p>Receives the updated JSON array of cues.<\/p>\n<\/li>\n<li>\n<p>Reconstructs the content back into a valid VTT file format.<\/p>\n<\/li>\n<li>\n<p>Uses the Cloudinary API to upload this new VTT content, overwriting the old file.<\/p>\n<\/li>\n<li>\n<p>Saves the updated JSON to our database.<\/p>\n<\/li>\n<\/ol>\n<p>This ensures that both our application\u2019s data and the source subtitle files on Cloudinary are always perfectly in sync.<\/p>\n<blockquote>\n<p>View the full API route on <a href=\"https:\/\/github.com\/musebe\/ai-video-insights\/blob\/main\/src\/app\/api\/transcript\/route.ts\">GitHub<\/a>.<\/p>\n<\/blockquote>\n<h2>Conclusion: The Power of Composable AI<\/h2>\n<p><a href=\"https:\/\/ai-video-insights.vercel.app\/\"><strong>Live Demo<\/strong><\/a> | <a href=\"https:\/\/github.com\/musebe\/ai-video-insights\"><strong>GitHub Repository<\/strong><\/a><\/p>\n<p>From a simple idea to a full-featured AI application, we\u2019ve completed the journey. By composing best-in-class APIs, we\u2019ve built a tool that does far more than just play videos, it understands them.<\/p>\n<p>We started with a robust foundation, using a <strong>Cloudinary Upload Preset<\/strong> to create a powerful, automated transcription pipeline. This handled all the heavy lifting of video processing, allowing us to focus on the application\u2019s intelligence. With a solid Next.js and Prisma backend, we securely managed our data and used a webhook to create a seamless, event-driven link back from Cloudinary.<\/p>\n<p>Finally, we integrated <strong>OpenAI<\/strong> and the <strong>Vercel AI SDK<\/strong> to bring the video\u2019s content to life. We didn\u2019t just display a transcript; we made it the foundation for summaries, social media content, and an interactive chat, turning a passive viewing experience into an active conversation.<\/p>\n<h2>Where to Go From Here<\/h2>\n<p>This project is a powerful starting point, but there are many exciting features you could add next. Cloudinary\u2019s APIs, in particular, offer a deep well of creative possibilities. Here are a few ideas:<\/p>\n<ul>\n<li>\n<p><strong>Dynamic subtitle styling.<\/strong> We built the foundation for this! You could expand the settings panel to allow users to change the <code>font_size<\/code>, <code>font_style<\/code>, or even add a background to the subtitles, all by dynamically adjusting the transformation parameters in the <code>VideoPlayer<\/code> component.<\/p>\n<\/li>\n<li>\n<p><strong>AI-generated highlight reels.<\/strong> Use Cloudinary\u2019s AI to automatically identify the most engaging moments in a video and create a short preview clip.<\/p>\n<\/li>\n<li>\n<p><strong>Chapter markers.<\/strong> Parse the transcript with an LLM to identify key topics and automatically generate chapter markers, allowing users to jump to specific sections of the video.<\/p>\n<\/li>\n<li>\n<p><strong>Translated subtitles.<\/strong> Integrate a translation service to offer subtitles in multiple languages, making your content globally accessible.<\/p>\n<\/li>\n<\/ul>\n<p>This project proves that by combining specialized APIs, a small team, or even a single developer, can build incredibly sophisticated AI applications. The future of web development is composable, and the tools are ready for you to build with. <a href=\"https:\/\/cloudinary.com\/users\/register_free\">Sign up for a free Cloudinary account<\/a> today to get started.<\/p>\n<\/div>","protected":false},"excerpt":{"rendered":"","protected":false},"author":87,"featured_media":38077,"comment_status":"closed","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"_cloudinary_featured_overwrite":false,"footnotes":""},"categories":[1],"tags":[336,212,303],"class_list":["post-38076","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized","tag-ai","tag-next-js","tag-video"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO Premium plugin v25.6 (Yoast SEO v26.9) - https:\/\/yoast.com\/product\/yoast-seo-premium-wordpress\/ -->\n<title>Building an AI Video Insights App With Next.js, Cloudinary, OpenAI, and Prisma<\/title>\n<meta name=\"description\" content=\"Learn to build a full-stack Next.js app that transforms videos into an interactive knowledge base. Use Cloudinary for video processing, OpenAI for AI-driven insights, and a webhook to connect them, enabling instant summaries, chat, and more.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/cloudinary.com\/blog\/ai-video-insights-app-next-js-cloudinary-openai-prisma\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Building an AI Video Insights App With Next.js, Cloudinary, OpenAI, and Prisma\" \/>\n<meta property=\"og:description\" content=\"Learn to build a full-stack Next.js app that transforms videos into an interactive knowledge base. Use Cloudinary for video processing, OpenAI for AI-driven insights, and a webhook to connect them, enabling instant summaries, chat, and more.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/cloudinary.com\/blog\/ai-video-insights-app-next-js-cloudinary-openai-prisma\" \/>\n<meta property=\"og:site_name\" content=\"Cloudinary Blog\" \/>\n<meta property=\"article:published_time\" content=\"2025-08-04T14:00:00+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1753480096\/Blog_AI-Powered_Video_Insights_with_Cloudinary_and_Next.js\/Blog_AI-Powered_Video_Insights_with_Cloudinary_and_Next.js.jpg?_i=AA\" \/>\n\t<meta property=\"og:image:width\" content=\"2000\" \/>\n\t<meta property=\"og:image:height\" content=\"1100\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"author\" content=\"melindapham\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"NewsArticle\",\"@id\":\"https:\/\/cloudinary.com\/blog\/ai-video-insights-app-next-js-cloudinary-openai-prisma#article\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/ai-video-insights-app-next-js-cloudinary-openai-prisma\"},\"author\":{\"name\":\"melindapham\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9\"},\"headline\":\"Building an AI Video Insights App With Next.js, Cloudinary, OpenAI, and Prisma\",\"datePublished\":\"2025-08-04T14:00:00+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/ai-video-insights-app-next-js-cloudinary-openai-prisma\"},\"wordCount\":13,\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/ai-video-insights-app-next-js-cloudinary-openai-prisma#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1753480096\/Blog_AI-Powered_Video_Insights_with_Cloudinary_and_Next.js\/Blog_AI-Powered_Video_Insights_with_Cloudinary_and_Next.js.jpg?_i=AA\",\"keywords\":[\"AI\",\"Next.js\",\"Video\"],\"inLanguage\":\"en-US\",\"copyrightYear\":\"2025\",\"copyrightHolder\":{\"@id\":\"https:\/\/cloudinary.com\/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/cloudinary.com\/blog\/ai-video-insights-app-next-js-cloudinary-openai-prisma\",\"url\":\"https:\/\/cloudinary.com\/blog\/ai-video-insights-app-next-js-cloudinary-openai-prisma\",\"name\":\"Building an AI Video Insights App With Next.js, Cloudinary, OpenAI, and Prisma\",\"isPartOf\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/ai-video-insights-app-next-js-cloudinary-openai-prisma#primaryimage\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/ai-video-insights-app-next-js-cloudinary-openai-prisma#primaryimage\"},\"thumbnailUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1753480096\/Blog_AI-Powered_Video_Insights_with_Cloudinary_and_Next.js\/Blog_AI-Powered_Video_Insights_with_Cloudinary_and_Next.js.jpg?_i=AA\",\"datePublished\":\"2025-08-04T14:00:00+00:00\",\"description\":\"Learn to build a full-stack Next.js app that transforms videos into an interactive knowledge base. Use Cloudinary for video processing, OpenAI for AI-driven insights, and a webhook to connect them, enabling instant summaries, chat, and more.\",\"breadcrumb\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/ai-video-insights-app-next-js-cloudinary-openai-prisma#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/cloudinary.com\/blog\/ai-video-insights-app-next-js-cloudinary-openai-prisma\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/ai-video-insights-app-next-js-cloudinary-openai-prisma#primaryimage\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1753480096\/Blog_AI-Powered_Video_Insights_with_Cloudinary_and_Next.js\/Blog_AI-Powered_Video_Insights_with_Cloudinary_and_Next.js.jpg?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1753480096\/Blog_AI-Powered_Video_Insights_with_Cloudinary_and_Next.js\/Blog_AI-Powered_Video_Insights_with_Cloudinary_and_Next.js.jpg?_i=AA\",\"width\":2000,\"height\":1100},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/cloudinary.com\/blog\/ai-video-insights-app-next-js-cloudinary-openai-prisma#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/cloudinary.com\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Building an AI Video Insights App With Next.js, Cloudinary, OpenAI, and Prisma\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#website\",\"url\":\"https:\/\/cloudinary.com\/blog\/\",\"name\":\"Cloudinary Blog\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/cloudinary.com\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#organization\",\"name\":\"Cloudinary Blog\",\"url\":\"https:\/\/cloudinary.com\/blog\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA\",\"contentUrl\":\"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA\",\"width\":312,\"height\":60,\"caption\":\"Cloudinary Blog\"},\"image\":{\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9\",\"name\":\"melindapham\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g\",\"caption\":\"melindapham\"}}]}<\/script>\n<!-- \/ Yoast SEO Premium plugin. -->","yoast_head_json":{"title":"Building an AI Video Insights App With Next.js, Cloudinary, OpenAI, and Prisma","description":"Learn to build a full-stack Next.js app that transforms videos into an interactive knowledge base. Use Cloudinary for video processing, OpenAI for AI-driven insights, and a webhook to connect them, enabling instant summaries, chat, and more.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/cloudinary.com\/blog\/ai-video-insights-app-next-js-cloudinary-openai-prisma","og_locale":"en_US","og_type":"article","og_title":"Building an AI Video Insights App With Next.js, Cloudinary, OpenAI, and Prisma","og_description":"Learn to build a full-stack Next.js app that transforms videos into an interactive knowledge base. Use Cloudinary for video processing, OpenAI for AI-driven insights, and a webhook to connect them, enabling instant summaries, chat, and more.","og_url":"https:\/\/cloudinary.com\/blog\/ai-video-insights-app-next-js-cloudinary-openai-prisma","og_site_name":"Cloudinary Blog","article_published_time":"2025-08-04T14:00:00+00:00","og_image":[{"width":2000,"height":1100,"url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1753480096\/Blog_AI-Powered_Video_Insights_with_Cloudinary_and_Next.js\/Blog_AI-Powered_Video_Insights_with_Cloudinary_and_Next.js.jpg?_i=AA","type":"image\/jpeg"}],"author":"melindapham","twitter_card":"summary_large_image","schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"NewsArticle","@id":"https:\/\/cloudinary.com\/blog\/ai-video-insights-app-next-js-cloudinary-openai-prisma#article","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/ai-video-insights-app-next-js-cloudinary-openai-prisma"},"author":{"name":"melindapham","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9"},"headline":"Building an AI Video Insights App With Next.js, Cloudinary, OpenAI, and Prisma","datePublished":"2025-08-04T14:00:00+00:00","mainEntityOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/ai-video-insights-app-next-js-cloudinary-openai-prisma"},"wordCount":13,"publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/ai-video-insights-app-next-js-cloudinary-openai-prisma#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1753480096\/Blog_AI-Powered_Video_Insights_with_Cloudinary_and_Next.js\/Blog_AI-Powered_Video_Insights_with_Cloudinary_and_Next.js.jpg?_i=AA","keywords":["AI","Next.js","Video"],"inLanguage":"en-US","copyrightYear":"2025","copyrightHolder":{"@id":"https:\/\/cloudinary.com\/#organization"}},{"@type":"WebPage","@id":"https:\/\/cloudinary.com\/blog\/ai-video-insights-app-next-js-cloudinary-openai-prisma","url":"https:\/\/cloudinary.com\/blog\/ai-video-insights-app-next-js-cloudinary-openai-prisma","name":"Building an AI Video Insights App With Next.js, Cloudinary, OpenAI, and Prisma","isPartOf":{"@id":"https:\/\/cloudinary.com\/blog\/#website"},"primaryImageOfPage":{"@id":"https:\/\/cloudinary.com\/blog\/ai-video-insights-app-next-js-cloudinary-openai-prisma#primaryimage"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/ai-video-insights-app-next-js-cloudinary-openai-prisma#primaryimage"},"thumbnailUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1753480096\/Blog_AI-Powered_Video_Insights_with_Cloudinary_and_Next.js\/Blog_AI-Powered_Video_Insights_with_Cloudinary_and_Next.js.jpg?_i=AA","datePublished":"2025-08-04T14:00:00+00:00","description":"Learn to build a full-stack Next.js app that transforms videos into an interactive knowledge base. Use Cloudinary for video processing, OpenAI for AI-driven insights, and a webhook to connect them, enabling instant summaries, chat, and more.","breadcrumb":{"@id":"https:\/\/cloudinary.com\/blog\/ai-video-insights-app-next-js-cloudinary-openai-prisma#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/cloudinary.com\/blog\/ai-video-insights-app-next-js-cloudinary-openai-prisma"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/ai-video-insights-app-next-js-cloudinary-openai-prisma#primaryimage","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1753480096\/Blog_AI-Powered_Video_Insights_with_Cloudinary_and_Next.js\/Blog_AI-Powered_Video_Insights_with_Cloudinary_and_Next.js.jpg?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1753480096\/Blog_AI-Powered_Video_Insights_with_Cloudinary_and_Next.js\/Blog_AI-Powered_Video_Insights_with_Cloudinary_and_Next.js.jpg?_i=AA","width":2000,"height":1100},{"@type":"BreadcrumbList","@id":"https:\/\/cloudinary.com\/blog\/ai-video-insights-app-next-js-cloudinary-openai-prisma#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/cloudinary.com\/blog\/"},{"@type":"ListItem","position":2,"name":"Building an AI Video Insights App With Next.js, Cloudinary, OpenAI, and Prisma"}]},{"@type":"WebSite","@id":"https:\/\/cloudinary.com\/blog\/#website","url":"https:\/\/cloudinary.com\/blog\/","name":"Cloudinary Blog","description":"","publisher":{"@id":"https:\/\/cloudinary.com\/blog\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/cloudinary.com\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/cloudinary.com\/blog\/#organization","name":"Cloudinary Blog","url":"https:\/\/cloudinary.com\/blog\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/","url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA","contentUrl":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1649718331\/Web_Assets\/blog\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877\/cloudinary_logo_for_white_bg_1937437aa7_19374666c7_193742f877.png?_i=AA","width":312,"height":60,"caption":"Cloudinary Blog"},"image":{"@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/0d5ad601e4c3b5be89245dfb14be42d9","name":"melindapham","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/cloudinary.com\/blog\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/e6f989fa97fe94be61596259d8629c3df65aec4c7da5c0000f90d810f313d4f4?s=96&d=mm&r=g","caption":"melindapham"}}]}},"jetpack_featured_media_url":"https:\/\/res.cloudinary.com\/cloudinary-marketing\/images\/f_auto,q_auto\/v1753480096\/Blog_AI-Powered_Video_Insights_with_Cloudinary_and_Next.js\/Blog_AI-Powered_Video_Insights_with_Cloudinary_and_Next.js.jpg?_i=AA","_links":{"self":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/38076","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/users\/87"}],"replies":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/comments?post=38076"}],"version-history":[{"count":1,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/38076\/revisions"}],"predecessor-version":[{"id":38081,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/posts\/38076\/revisions\/38081"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media\/38077"}],"wp:attachment":[{"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/media?parent=38076"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/categories?post=38076"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cloudinary.com\/blog\/wp-json\/wp\/v2\/tags?post=38076"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}