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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 52 additions & 1 deletion src/app/api/creations/[id]/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export async function GET(req, { params }) {

const { id } = await params;

const creation = await prisma.creation.findUnique({
let creation = await prisma.creation.findUnique({
where: {
id,
userId: session.user.id // Security check
Expand All @@ -24,6 +24,57 @@ export async function GET(req, { params }) {
return new NextResponse("Not Found", { status: 404 });
}

// Active polling fallback in case webhook failed to deliver or wasn't set up
const activeStatuses = ['processing', 'pending', 'starting', 'queued'];
if (activeStatuses.includes(creation.status) && creation.requestId) {
const apiKey = process.env.UGC_API_KEY;
if (apiKey) {
try {
const checkRes = await fetch(`https://api.muapi.ai/api/v1/predictions/${creation.requestId}/result`, {
method: "GET",
headers: {
"x-api-key": apiKey,
},
});

if (checkRes.ok) {
const checkData = await checkRes.json();
const upstreamStatus = checkData.status || "processing";

if (upstreamStatus === "failed") {
creation = await prisma.creation.update({
where: { id },
data: {
status: "failed",
error: checkData.error || "Generation failed"
}
});
} else if (upstreamStatus === "completed" || (checkData.outputs && checkData.outputs.length > 0)) {
const outputs = checkData.outputs || [];
const videoUrl = outputs.length > 0 ? outputs[0] : null;

creation = await prisma.creation.update({
where: { id },
data: {
status: "completed",
url: videoUrl
}
});
} else if (upstreamStatus !== "processing") {
creation = await prisma.creation.update({
where: { id },
data: {
status: upstreamStatus
}
});
}
}
} catch (pollErr) {
console.error("Error polling prediction status from upstream:", pollErr);
}
}
}

return NextResponse.json(creation);

} catch (error) {
Expand Down
29 changes: 25 additions & 4 deletions src/app/api/generate/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,14 +30,35 @@ export async function POST(req) {
return new NextResponse("Invalid model selected", { status: 400 });
}

// Calculate required credits
let requiredCredits = 10;
const duration = typeof settings.duration === "number" ? settings.duration : 5;
const resolution = settings.resolution || "";

if (modelId === "grok-video") {
const grokDuration = typeof settings.duration === "number" ? settings.duration : 6;
const rate = resolution === "720p" ? 10 : 5;
requiredCredits = grokDuration * rate;
} else if (modelId === "veo-3-1") {
const veoDuration = typeof settings.duration === "number" ? settings.duration : 8;
let rate = 500;
if (resolution === "1080p") rate = 650;
else if (resolution === "4k") rate = 740;
requiredCredits = veoDuration * rate;
} else if (modelId === "happy-horse") {
requiredCredits = duration * 36;
} else if (modelId === "seedance-2") {
requiredCredits = duration * 50;
}

// Check credits
const user = await prisma.user.findUnique({
where: { id: session.user.id },
select: { credits: true }
});

if (!user || user.credits <= 0) {
return NextResponse.json({ error: "Insufficient credits" }, { status: 403 });
if (!user || user.credits < requiredCredits) {
return NextResponse.json({ error: `Insufficient credits. This requires ${requiredCredits} credits but you only have ${user?.credits || 0}.` }, { status: 403 });
}

// Prepare payload based on MUAPI specs
Expand Down Expand Up @@ -84,10 +105,10 @@ export async function POST(req) {
}
});

// Deduct 1 credit
// Deduct required credits
await prisma.user.update({
where: { id: session.user.id },
data: { credits: { decrement: 1 } }
data: { credits: { decrement: requiredCredits } }
});

return NextResponse.json({
Expand Down
50 changes: 46 additions & 4 deletions src/app/page.js
Original file line number Diff line number Diff line change
Expand Up @@ -207,15 +207,18 @@ export default function Home() {
// Polling for last generation status
useEffect(() => {
let interval;
if (lastGeneration && lastGeneration.status === 'processing') {
const activeStatuses = ['processing', 'pending', 'starting', 'queued'];
if (lastGeneration && activeStatuses.includes(lastGeneration.status)) {
interval = setInterval(async () => {
try {
const res = await fetch(`/api/creations/${lastGeneration.id}`);
if (res.ok) {
const data = await res.json();
if (data.status !== 'processing') {
if (!activeStatuses.includes(data.status)) {
setLastGeneration(data);
clearInterval(interval);
} else if (data.status !== lastGeneration.status) {
setLastGeneration(data);
}
}
} catch (error) {
Expand All @@ -236,6 +239,35 @@ export default function Home() {
}
}, [selectedModel]);

const getRequiredCredits = () => {
const duration = typeof modelSettings.duration === "number" ? modelSettings.duration : 5;
const resolution = modelSettings.resolution || "";

if (selectedModel.id === "grok-video") {
const grokDuration = typeof modelSettings.duration === "number" ? modelSettings.duration : 6;
const rate = resolution === "720p" ? 10 : 5;
return grokDuration * rate;
}

if (selectedModel.id === "veo-3-1") {
const veoDuration = typeof modelSettings.duration === "number" ? modelSettings.duration : 8;
let rate = 500;
if (resolution === "1080p") rate = 650;
else if (resolution === "4k") rate = 740;
return veoDuration * rate;
}

if (selectedModel.id === "happy-horse") {
return duration * 36;
}

if (selectedModel.id === "seedance-2") {
return duration * 50;
}

return 10;
};


const handleImageUpload = async (e) => {
const files = Array.from(e.target.files);
Expand Down Expand Up @@ -364,10 +396,12 @@ export default function Home() {
animate={{ opacity: 1, scale: 1, y: 0 }}
className="relative w-full max-w-lg aspect-[9/16] max-h-[60vh] bg-glass-bg rounded border border-glass-border shadow-2xl overflow-hidden flex flex-col items-center justify-center"
>
{lastGeneration.status === 'processing' ? (
{['processing', 'pending', 'starting', 'queued'].includes(lastGeneration.status) ? (
<div className="flex flex-col items-center gap-4">
<div className="w-12 h-12 border-4 border-primary-200 border-t-primary-500 rounded-full animate-spin" />
<span className="text-[10px] font-black text-muted uppercase tracking-[0.3em] animate-pulse">Manifesting...</span>
<span className="text-[10px] font-black text-muted uppercase tracking-[0.3em] animate-pulse">
Manifesting ({lastGeneration.status})...
</span>
</div>
) : lastGeneration.status === 'failed' ? (
<div className="flex flex-col items-center gap-4 p-8 text-center">
Expand Down Expand Up @@ -552,6 +586,14 @@ export default function Home() {
</div>
</div>

{/* Show credit cost */}
<div className="flex items-center gap-1.5 px-3 py-1.5 bg-[#f9f9f9] border border-[#ececec] rounded-full mr-2">
<FaCoins className="text-yellow-600 text-xs" />
<span className="text-[10px] font-bold text-slate-700">
Cost: {getRequiredCredits()}
</span>
</div>

<button
onClick={handleGenerate}
disabled={isGenerating || !prompt.trim()}
Expand Down
11 changes: 11 additions & 0 deletions src/components/saas/Navbar.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import Link from "next/link";
import { usePathname } from "next/navigation";
import { signOut, useSession } from "next-auth/react";
import { FaCoins, FaUser, FaSignOutAlt, FaChevronDown, FaRocket, FaBars, FaTimes } from "react-icons/fa";
import { SiVercel } from "react-icons/si";
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import { LoginButton } from "./AuthButtons";
Expand Down Expand Up @@ -117,6 +118,16 @@ export function Navbar() {
) : (
<LoginButton className="!h-10 !px-6 !text-[10px] !tracking-widest !font-bold" />
)}

<a
href="https://vercel.com/new/clone?repository-url=https://github.com/Anil-matcha/Open-AI-UGC"
target="_blank"
rel="noopener noreferrer"
className="hidden lg:flex items-center gap-2 px-4 py-2 rounded-xl bg-slate-900 border border-slate-800 text-white hover:bg-slate-800 transition-all font-bold text-[10px] tracking-widest uppercase shadow-lg shadow-slate-900/10"
>
<SiVercel className="text-xs" />
Deploy
</a>

<button
className="md:hidden p-2 text-muted hover:text-foreground"
Expand Down