diff --git a/package-lock.json b/package-lock.json index 1840cc3..125446d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,16 +8,19 @@ "name": "ai-podcast", "version": "0.1.0", "dependencies": { + "@ai-sdk/mistral": "^2.0.15", "@mendable/firecrawl-js": "^4.3.5", "@radix-ui/react-progress": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", + "ai": "^5.0.48", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.544.0", "next": "15.5.3", "react": "19.1.0", "react-dom": "19.1.0", - "tailwind-merge": "^3.3.1" + "tailwind-merge": "^3.3.1", + "zod": "^3.25.76" }, "devDependencies": { "@eslint/eslintrc": "^3", @@ -32,6 +35,67 @@ "typescript": "^5" } }, + "node_modules/@ai-sdk/gateway": { + "version": "1.0.25", + "resolved": "https://registry.npmjs.org/@ai-sdk/gateway/-/gateway-1.0.25.tgz", + "integrity": "sha512-eI/6LLmn1tWFzuhjxgcPEqUFXwLjyRuGFrwkCoqLaTKe/qMYBEAV3iddnGUM0AV+Hp4NEykzP4ly5tibOLDMXw==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.9" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4" + } + }, + "node_modules/@ai-sdk/mistral": { + "version": "2.0.15", + "resolved": "https://registry.npmjs.org/@ai-sdk/mistral/-/mistral-2.0.15.tgz", + "integrity": "sha512-vaNNd+Ekjosizsm4r23UUZnMNY++3HtGadTZ/BcY6eCrIq9sk+KMNfu4HO7ojlcm0P4/kuPMyiq38Me46C7O+Q==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.9" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4" + } + }, + "node_modules/@ai-sdk/provider": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider/-/provider-2.0.0.tgz", + "integrity": "sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==", + "license": "Apache-2.0", + "dependencies": { + "json-schema": "^0.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@ai-sdk/provider-utils": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@ai-sdk/provider-utils/-/provider-utils-3.0.9.tgz", + "integrity": "sha512-Pm571x5efqaI4hf9yW4KsVlDBDme8++UepZRnq+kqVBWWjgvGhQlzU8glaFq0YJEB9kkxZHbRRyVeHoV2sRYaQ==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/provider": "2.0.0", + "@standard-schema/spec": "^1.0.0", + "eventsource-parser": "^3.0.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4" + } + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -982,6 +1046,15 @@ "node": ">=12.4.0" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", @@ -1091,6 +1164,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.15", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", @@ -2018,6 +2097,24 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/ai": { + "version": "5.0.48", + "resolved": "https://registry.npmjs.org/ai/-/ai-5.0.48.tgz", + "integrity": "sha512-+oYhbN3NGRXayGfTFI8k1Fu4rhiJcQ0mbgiAOJGFkzvCxunRRQu5cyDl7y6cHNTj1QvHmIBROK5u655Ss2oI0g==", + "license": "Apache-2.0", + "dependencies": { + "@ai-sdk/gateway": "1.0.25", + "@ai-sdk/provider": "2.0.0", + "@ai-sdk/provider-utils": "3.0.9", + "@opentelemetry/api": "1.9.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "zod": "^3.25.76 || ^4" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -3324,6 +3421,15 @@ "node": ">=0.10.0" } }, + "node_modules/eventsource-parser": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.0.6.tgz", + "integrity": "sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4291,6 +4397,12 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", diff --git a/package.json b/package.json index 47dd5b1..29a29f7 100644 --- a/package.json +++ b/package.json @@ -9,16 +9,19 @@ "lint": "eslint" }, "dependencies": { + "@ai-sdk/mistral": "^2.0.15", "@mendable/firecrawl-js": "^4.3.5", "@radix-ui/react-progress": "^1.1.7", "@radix-ui/react-slot": "^1.2.3", + "ai": "^5.0.48", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "lucide-react": "^0.544.0", "next": "15.5.3", "react": "19.1.0", "react-dom": "19.1.0", - "tailwind-merge": "^3.3.1" + "tailwind-merge": "^3.3.1", + "zod": "^3.25.76" }, "devDependencies": { "@eslint/eslintrc": "^3", diff --git a/src/app/api/generate-conversation/route.ts b/src/app/api/generate-conversation/route.ts new file mode 100644 index 0000000..216df90 --- /dev/null +++ b/src/app/api/generate-conversation/route.ts @@ -0,0 +1,81 @@ +import { NextRequest, NextResponse } from 'next/server'; +import { generateObject } from 'ai'; +import { mistral } from '@ai-sdk/mistral'; +import { z } from 'zod'; + +const messageSchema = z.object({ + id: z.string(), + speaker: z.enum(['host1', 'host2']), + text: z.string(), + timestamp: z.string(), +}); + +const conversationSchema = z.object({ + messages: z.array(messageSchema), + detectedLanguage: z.string(), +}); + +export async function POST(request: NextRequest) { + try { + const { content, title, url } = await request.json(); + + if (!content) { + return NextResponse.json( + { error: 'Content is required' }, + { status: 400 } + ); + } + + console.log('Generating conversation for:', { title, url, contentLength: content.length }); + + // Generate podcast conversation using Mistral + const { object } = await generateObject({ + model: mistral('mistral-medium-latest'), + schema: conversationSchema, + schemaName: 'PodcastConversation', + schemaDescription: 'A podcast-style conversation between two hosts discussing scraped content', + prompt: `You are generating a podcast conversation between two hosts discussing the following scraped content from "${title}" at ${url}. + +CONTENT: +${content} + +Generate a natural, engaging podcast conversation with exactly 10 turns (5 per host). The conversation should: + +1. HOST 1 PERSONALITY: Bubbly, excited, enthusiastic, and optimistic. Uses expressions like "Wow!", "Amazing!", "That's so cool!". Often laughs [giggles] and shows genuine excitement. + +2. HOST 2 PERSONALITY: Skeptical, sarcastic, and thoughtful. Uses dry humor and irony. Often makes sarcastic comments [sarcastically] and plays devil's advocate. + +3. Use the same language as the content (if content is in English, respond in English; if in Italian, respond in Italian, etc.) + +4. Include emotional expressions in brackets like [giggles], [laughs], [sarcastically], [whispers], [excitedly], [thoughtfully], etc. + +5. Make it sound natural and conversational, like a real podcast + +6. Include timestamps in MM:SS format (starting from 0:15 and incrementing by 20-30 seconds each) + +7. The conversation should flow naturally and cover the main points of the content + +Format the response as a structured object with messages array and detected language.`, + temperature: 0.7, + maxTokens: 2000, + }); + + console.log('Conversation generated successfully'); + + return NextResponse.json({ + success: true, + data: { + messages: object.messages, + detectedLanguage: object.detectedLanguage, + generatedAt: new Date().toISOString() + } + }); + + } catch (error) { + console.error('Conversation generation error:', error); + return NextResponse.json( + { error: error instanceof Error ? error.message : 'Failed to generate conversation' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/app/page.tsx b/src/app/page.tsx index c53248d..2f71fd6 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -28,7 +28,7 @@ export default function Home() { excerpt: string; scrapedAt: string; }>>([]); - + const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsLoading(true); @@ -54,14 +54,9 @@ export default function Home() { scrapedAt: result.data.scrapedAt }, ...prev]); - // Show success message - setMessages([{ - id: '1', - speaker: 'host1', - text: `Great! I've successfully scraped the content from "${result.data.title || 'the website'}". The content has been processed and is ready for podcast generation.`, - timestamp: '0:15' - }]); - setDuration(320); + + // Generate conversation + await generateConversation(result.data.content, result.data.title || 'Untitled', result.data.url); } else { throw new Error(result.error || 'Failed to scrape website'); } @@ -78,6 +73,39 @@ export default function Home() { } }; + const generateConversation = async (content: string, title: string, url: string) => { + try { + const response = await fetch('/api/generate-conversation', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ content, title, url }), + }); + + const result = await response.json(); + + if (result.success) { + setMessages(result.data.messages); + + // Calculate duration based on conversation length + const avgTimePerMessage = 25; // seconds + const totalDuration = result.data.messages.length * avgTimePerMessage; + setDuration(totalDuration); + } else { + throw new Error(result.error || 'Failed to generate conversation'); + } + } catch (error) { + console.error('Conversation generation error:', error); + setMessages([{ + id: '1', + speaker: 'host1', + text: 'Sorry, I encountered an error while generating the podcast conversation. Please try again.', + timestamp: '0:15' + }]); + } + }; + const togglePlay = () => { if (isPlaying) { setIsPlaying(false); @@ -149,7 +177,7 @@ export default function Home() { className="w-full" disabled={isLoading || !url} > - {isLoading ? 'Generating...' : 'Generate Podcast'} + {isLoading ? 'Scraping & Generating...' : 'Generate Podcast'} @@ -185,7 +213,7 @@ export default function Home() { {messages.length > 0 && (

- Website scraped successfully! Content is ready for podcast generation. + Podcast conversation generated successfully!

)} @@ -245,6 +273,12 @@ export default function Home() { ) : (
+
+

+ AI-generated conversation ready! Click play to listen. +

+
+ {/* Audio Controls */}