diff --git a/apps/web/src/app/api/pipeline/route.ts b/apps/web/src/app/api/pipeline/route.ts new file mode 100644 index 000000000..bfb9cab57 --- /dev/null +++ b/apps/web/src/app/api/pipeline/route.ts @@ -0,0 +1,163 @@ +import { NextResponse } from 'next/server'; +import { publishEvent, EventTypes } from '@/lib/cloudevents'; +import { analyzeVideoWithGemini } from '@/lib/gemini-video-analyzer'; +import { hasGeminiKey } from '@/lib/gemini-client'; + +const rawBackendUrl = process.env.BACKEND_URL || ''; +const BACKEND_URL = rawBackendUrl.startsWith('http') ? rawBackendUrl : 'http://localhost:8000'; +const BACKEND_AVAILABLE = rawBackendUrl.startsWith('http'); + +/** + * POST /api/pipeline + * + * End-to-end pipeline: YouTube URL → Video Analysis → Code Generation → Deployment → Live URL + * + * This is the FULL pipeline that the user's notes describe (PK=999, PK=1021): + * Ingest → Translate → Transport → Execute + * + * Strategies: + * 1. Backend pipeline (FastAPI /api/v1/video-to-software) — full pipeline with agents + * 2. Gemini analysis + frontend deployment — when no backend is available + */ +export async function POST(request: Request) { + let videoUrl: string | undefined; + try { + const body = await request.json(); + const { url, project_type = 'web', deployment_target = 'vercel', features } = body; + videoUrl = url; + + if (!url) { + return NextResponse.json({ error: 'Video URL is required' }, { status: 400 }); + } + + await publishEvent(EventTypes.VIDEO_RECEIVED, { url, pipeline: 'end-to-end' }, url); + + // ── Strategy 1: Full backend pipeline (FastAPI video-to-software) ── + if (BACKEND_AVAILABLE) { + try { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 300_000); // 5 min for full pipeline + + let response: Response; + try { + response = await fetch(`${BACKEND_URL}/api/v1/video-to-software`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + video_url: url, + project_type, + deployment_target, + features: features || ['responsive_design', 'modern_ui'], + }), + signal: controller.signal, + }); + } finally { + clearTimeout(timeout); + } + + if (response.ok) { + const result = await response.json(); + + await publishEvent(EventTypes.PIPELINE_COMPLETED, { + strategy: 'backend-pipeline', + success: result.status === 'success', + live_url: result.live_url, + github_repo: result.github_repo, + build_status: result.build_status, + }, url); + + return NextResponse.json({ + id: `pipeline_${Date.now().toString(36)}`, + status: result.status || 'complete', + pipeline: 'backend', + processing_time: result.processing_time, + result: { + live_url: result.live_url, + github_repo: result.github_repo, + build_status: result.build_status, + video_analysis: result.video_analysis, + code_generation: result.code_generation, + deployment: result.deployment, + features_implemented: result.features_implemented, + }, + }); + } + console.warn(`Backend pipeline returned ${response.status}, falling back`); + } catch (e) { + console.log('Backend pipeline unavailable:', e); + } + } + + // ── Strategy 2: Gemini analysis (video intelligence only, no deployment) ── + if (hasGeminiKey()) { + try { + const startTime = Date.now(); + const analysis = await analyzeVideoWithGemini(url); + const elapsed = Date.now() - startTime; + + await publishEvent(EventTypes.PIPELINE_COMPLETED, { + strategy: 'gemini-analysis-only', + success: true, + note: 'Backend unavailable — analysis only, no deployment', + }, url); + + return NextResponse.json({ + id: `pipeline_${Date.now().toString(36)}`, + status: 'partial', + pipeline: 'gemini-only', + processing_time: `${(elapsed / 1000).toFixed(1)}s`, + result: { + live_url: null, + github_repo: null, + build_status: 'not_attempted', + video_analysis: { + title: analysis.title, + summary: analysis.summary, + events: analysis.events, + actions: analysis.actions, + topics: analysis.topics, + architectureCode: analysis.architectureCode, + }, + code_generation: null, + deployment: null, + message: 'Backend pipeline unavailable. Video analysis complete but code generation and deployment require the Python backend.', + }, + }); + } catch (e) { + console.error('Gemini analysis failed:', e); + } + } + + return NextResponse.json( + { error: 'No pipeline available. Configure BACKEND_URL for full pipeline or GEMINI_API_KEY for analysis only.' }, + { status: 503 }, + ); + } catch (error) { + console.error('Pipeline error:', error); + await publishEvent(EventTypes.PIPELINE_FAILED, { error: String(error) }, videoUrl).catch(() => {}); + return NextResponse.json( + { error: 'Pipeline failed', details: String(error) }, + { status: 500 }, + ); + } +} + +export async function GET() { + return NextResponse.json({ + name: 'EventRelay End-to-End Pipeline', + version: '1.0.0', + description: 'YouTube URL → Video Analysis → Code Generation → Deployment → Live URL', + pipeline_stages: [ + '1. Ingest: Gemini analyzes video content with Google Search grounding', + '2. Translate: Structured output → VideoPack artifact', + '3. Transport: CloudEvents published at each stage', + '4. Execute: Agents generate code, create repo, deploy to Vercel', + ], + backend_available: BACKEND_AVAILABLE, + gemini_available: hasGeminiKey(), + endpoints: { + pipeline: 'POST /api/pipeline - Full end-to-end pipeline', + video: 'POST /api/video - Video analysis only', + }, + }); +} diff --git a/apps/web/src/app/api/video/route.ts b/apps/web/src/app/api/video/route.ts index 244fbb56e..fc59a8425 100644 --- a/apps/web/src/app/api/video/route.ts +++ b/apps/web/src/app/api/video/route.ts @@ -39,6 +39,8 @@ export async function POST(request: Request) { await publishEvent(EventTypes.VIDEO_RECEIVED, { url }, url); // ── Strategy 1: Full backend pipeline (skip if no backend configured) ── + // Calls /api/v1/transcript-action for analysis. For full end-to-end + // pipeline (analysis → code gen → deploy), use POST /api/pipeline instead. if (BACKEND_AVAILABLE) { try { const controller = new AbortController(); @@ -273,6 +275,7 @@ export async function GET() { frontend_pipeline: 'active', endpoints: { analyze: 'POST /api/video - Analyze a video URL', + pipeline: 'POST /api/pipeline - Full end-to-end pipeline (YouTube URL → deployed software)', }, }); } diff --git a/apps/web/src/app/dashboard/page.tsx b/apps/web/src/app/dashboard/page.tsx index 4a9b410f2..59df9ede2 100644 --- a/apps/web/src/app/dashboard/page.tsx +++ b/apps/web/src/app/dashboard/page.tsx @@ -9,28 +9,7 @@ import TranscriptViewer from '@/components/TranscriptViewer'; import EventList from '@/components/EventList'; import type { ExtractedEvent } from '@/lib/types'; import { useDashboardStore } from '@/store/dashboard-store'; - -// ============================================ -// Types -// ============================================ -interface Video { - id: string; - title: string; - url: string; - status: 'processing' | 'complete' | 'failed'; - progress: number; - thumbnail?: string; - duration?: string; - processedAt?: string; - transcript?: string; - events?: ExtractedEvent[]; - insights?: { - summary: string; - actions: string[]; - sentiment: string; - topics: string[]; - }; -} +import type { PipelineResult, Video } from '@/store/dashboard-store'; // ============================================ // Processing Stage Indicator @@ -75,7 +54,9 @@ function VideoCard({ video: Video; onClick: () => void; }) { - const stages = ['Ingest', 'Transcribe', 'Analyze', 'Extract']; + const stages = video.pipelineResult !== undefined + ? ['Ingest', 'Generate', 'Deploy', 'Live'] + : ['Ingest', 'Transcribe', 'Analyze', 'Extract']; const currentStage = Math.floor((video.progress / 100) * stages.length); return ( @@ -173,6 +154,38 @@ function VideoCard({ {/* Quick insights for completed */} {video.status === 'complete' && video.insights && (
+ {/* Pipeline deployment result */} + {video.pipelineResult && ( +
+ {video.pipelineResult.live_url && ( + + 🌐 Live: {video.pipelineResult.live_url} + + )} + {video.pipelineResult.github_repo && ( + + 📦 Repo: {video.pipelineResult.github_repo} + + )} + {video.pipelineResult.code_generation && ( +
+ Framework: {video.pipelineResult.code_generation.framework} + + {video.pipelineResult.code_generation.files_created.length} files +
+ )} +
+ )}
{video.insights.topics.slice(0, 3).map((topic) => ( s.selectedVideoId); const selectVideo = useDashboardStore((s) => s.selectVideo); const processVideo = useDashboardStore((s) => s.processVideo); + const deployPipeline = useDashboardStore((s) => s.deployPipeline); const extractEvents = useDashboardStore((s) => s.extractEvents); const selectedVideo = videos.find((v) => v.id === selectedVideoId) || null; @@ -537,6 +551,16 @@ function DashboardContent() { [videoUrl, processVideo], ); + const handleDeployPipeline = useCallback( + () => { + const targetUrl = videoUrl; + if (!targetUrl.trim()) return; + setVideoUrl(''); + deployPipeline(targetUrl); + }, + [videoUrl, deployPipeline], + ); + return (
{/* Navigation */} @@ -607,6 +631,14 @@ function DashboardContent() { > Analyze +
diff --git a/apps/web/src/store/dashboard-store.ts b/apps/web/src/store/dashboard-store.ts index d84595f44..2dc3ff2c5 100644 --- a/apps/web/src/store/dashboard-store.ts +++ b/apps/web/src/store/dashboard-store.ts @@ -14,6 +14,22 @@ import type { // ── Types ── +export interface PipelineResult { + live_url: string | null; + github_repo: string | null; + build_status: string; + code_generation: { + framework: string; + files_created: string[]; + entry_point: string; + } | null; + deployment: { + status: string; + platforms: string[]; + urls: Record; + } | null; +} + export interface Video { id: string; title: string; @@ -26,6 +42,7 @@ export interface Video { transcript?: string; events?: ExtractedEvent[]; agents?: AgentExecution[]; + pipelineResult?: PipelineResult; insights?: { summary: string; actions: string[]; @@ -60,6 +77,7 @@ interface DashboardState { // Workflow actions processVideo: (url: string) => Promise; + deployPipeline: (url: string) => Promise; extractEvents: (videoId: string) => void; dispatchAgents: (videoId: string) => void; } @@ -240,6 +258,86 @@ export const useDashboardStore = create((set, get) => ({ } }, + // ── Full end-to-end pipeline: YouTube URL → deployed software ── + deployPipeline: async (url) => { + const { addVideo, updateVideo, addActivity } = get(); + const id = Date.now().toString(); + + const video: Video = { + id, + title: `🚀 Deploying: ${url.length > 40 ? url.substring(0, 37) + '…' : url}`, + url, + status: 'processing', + progress: 5, + }; + addVideo(video); + addActivity(`Pipeline started: ${url.length > 40 ? url.substring(0, 37) + '…' : url}`, 'info'); + + const stages = ['Analyzing video', 'Generating code', 'Creating repo', 'Deploying']; + let stageIdx = 0; + const interval = setInterval(() => { + const current = get().videos.find((v) => v.id === id); + if (current && current.status === 'processing') { + const newProgress = Math.min(current.progress + 3, 95); + const newStage = Math.min(Math.floor(newProgress / 25), stages.length - 1); + if (newStage > stageIdx) { + stageIdx = newStage; + addActivity(stages[stageIdx] + '…', 'info'); + } + updateVideo(id, { progress: newProgress }); + } + }, 2000); + + try { + const res = await fetch('/api/pipeline', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url, project_type: 'web', deployment_target: 'vercel' }), + }); + clearInterval(interval); + + if (!res.ok) throw new Error(`Pipeline error: ${res.status}`); + + const result = await res.json(); + const pipelineResult: PipelineResult = { + live_url: result.result?.live_url || null, + github_repo: result.result?.github_repo || null, + build_status: result.result?.build_status || 'unknown', + code_generation: result.result?.code_generation || null, + deployment: result.result?.deployment || null, + }; + + updateVideo(id, { + status: result.status === 'success' || result.status === 'complete' ? 'complete' : 'failed', + progress: 100, + title: `Deployed: ${url.length > 40 ? url.substring(0, 37) + '…' : url}`, + processedAt: 'Just now', + pipelineResult, + insights: { + summary: result.result?.video_analysis?.extracted_info?.title || 'Pipeline complete', + actions: result.result?.features_implemented || [], + sentiment: 'Positive', + topics: result.result?.code_generation?.files_created || [], + }, + }); + + if (pipelineResult.live_url) { + addActivity(`🎉 Live at: ${pipelineResult.live_url}`, 'success'); + } + if (pipelineResult.github_repo) { + addActivity(`📦 Repo: ${pipelineResult.github_repo}`, 'success'); + } + addActivity(`Pipeline complete (${result.processing_time || 'done'})`, 'success'); + } catch (error) { + clearInterval(interval); + updateVideo(id, { status: 'failed', progress: 0 }); + addActivity( + `Pipeline failed: ${error instanceof Error ? error.message : 'Unknown error'}`, + 'error', + ); + } + }, + // ── Extract events from a completed video ── extractEvents: (videoId) => { const { videos, updateVideo, addActivity } = get(); diff --git a/src/youtube_extension/backend/services/video_processing_service.py b/src/youtube_extension/backend/services/video_processing_service.py index aaf32784b..9a4f44324 100644 --- a/src/youtube_extension/backend/services/video_processing_service.py +++ b/src/youtube_extension/backend/services/video_processing_service.py @@ -330,7 +330,7 @@ async def process_video_to_software(self, video_url: str, project_type: str = "w # Import required components try: from youtube_extension.backend.code_generator import get_code_generator - from youtube_extension.backend.services.deployment_manager import ( + from youtube_extension.backend.deployment_manager import ( get_deployment_manager, ) pipeline_available = True