| name | atlas-marketing-studio |
| description | Self-hostable AI video ad studio for e-commerce with UGC product ads, reference-ad remakes, AI drama ads, and ad skits powered by Atlas Cloud |
| triggers | ["generate AI video ads for ecommerce products","create UGC product video advertisements","remake reference ads with new products","build AI drama ads for social commerce","generate short ad skits with AI","set up Atlas Marketing Studio","configure video ad generation workflow","implement AI video credit system"] |
Atlas Marketing Studio Skill
Skill by ara.so — Marketing Skills collection.
Atlas Marketing Studio is a self-hostable AI video ad studio for e-commerce that generates UGC product ads, reference-ad remakes, AI drama ads, and short ad skits. Built on TypeScript/Next.js, it orchestrates multiple AI models through Atlas Cloud API for complete ad production workflows with credit metering, authentication, and payment integration.
Installation
git clone https://github.com/AtlasCloudAI/atlas-marketing-studio.git
cd atlas-marketing-studio
npm install
Environment Setup
Create .env for Vercel deployment:
ATLASCLOUD_API_KEY=your_atlas_key
NEXTAUTH_SECRET=your_nextauth_secret
NEXTAUTH_URL=http://localhost:3000
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
PAYMENT_PROVIDER=atlas
DATABASE_URL=your_neon_postgres_url
BLOB_READ_WRITE_TOKEN=your_vercel_blob_token
Or .dev.vars for Cloudflare deployment (uses D1/R2 bindings from wrangler.jsonc):
ATLASCLOUD_API_KEY=your_atlas_key
NEXTAUTH_SECRET=your_nextauth_secret
NEXTAUTH_URL=http://localhost:8788
GOOGLE_CLIENT_ID=your_google_client_id
GOOGLE_CLIENT_SECRET=your_google_client_secret
PAYMENT_PROVIDER=atlas
Database Initialization
For Vercel (Neon Postgres):
npm run db:push:vercel
For Cloudflare (D1):
npm run db:push:cf
Key Commands
npm run dev:vercel
npm run cf:preview
npm run build
vercel deploy
npm run cf:deploy
npm run db:push:vercel
npm run db:push:cf
npm run db:studio
wrangler secret put ATLASCLOUD_API_KEY
wrangler secret put NEXTAUTH_SECRET
Core Workflows
1. UGC Product Ad Generation
Generates lip-synced UGC ads from product and presenter photos.
API Route: /api/generate-ugc-ad
import { generateUGCAd } from '@/lib/marketing-studio/ugc-generator';
const result = await generateUGCAd({
productImageUrl: 'https://example.com/product.jpg',
presenterImageUrl: 'https://example.com/presenter.jpg',
productName: 'Wireless Earbuds',
targetAudience: 'fitness enthusiasts',
tone: 'enthusiastic',
duration: 15,
atlasApiKey: process.env.ATLASCLOUD_API_KEY!,
});
UI Component Example:
'use client';
import { useState } from 'react';
import { uploadImage } from '@/lib/upload';
export default function MarketingStudioPage() {
const [productImage, setProductImage] = useState<File | null>(null);
const [presenterImage, setPresenterImage] = useState<File | null>(null);
const [productName, setProductName] = useState('');
const [generating, setGenerating] = useState(false);
const handleGenerate = async () => {
setGenerating(true);
const productUrl = await uploadImage(productImage!);
const presenterUrl = await uploadImage(presenterImage!);
const response = await fetch('/api/generate-ugc-ad', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: .({
: productUrl,
: presenterUrl,
productName,
}),
});
result = response.();
();
};
(
);
}
2. Reference Ad Remake
Remakes existing video ads with new products and presenters.
export async function remakeReferenceAd({
referenceVideoUrl,
productImageUrl,
presenterImageUrl,
apiKey,
}: {
referenceVideoUrl: string;
productImageUrl: string;
presenterImageUrl: string;
apiKey: string;
}) {
const response = await fetch('https://api.atlascloud.ai/v1/video/edit', {
method: 'POST',
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'gemini-omni-flash/video-edit',
referenceVideo: referenceVideoUrl,
replacements: {
product: productImageUrl,
presenter: presenterImageUrl,
},
}),
});
return await response.json();
}
3. AI Drama Ad Generation
Creates multi-shot story ads from a single topic.
import { generateDramaScript } from './script-generator';
import { generateShotVideo } from './shot-generator';
export async function generateDramaAd({
topic,
language = 'en',
shotCount = 4,
atlasApiKey,
}: {
topic: string;
language?: string;
shotCount?: number;
atlasApiKey: string;
}) {
const script = await generateDramaScript({
topic,
language,
shotCount,
atlasApiKey,
});
const shots = await Promise.all(
script.shots.map((shot) =>
generateShotVideo({
shotDescription: shot.description,
dialogue: shot.dialogue,
referenceImage: shot.referenceImage,
atlasApiKey,
})
)
);
const finalVideo = await concatenateVideos(shots.map(s => s.videoUrl));
return {
script,
shots,
: finalVideo,
: (shots),
};
}
4. Ad Skit Generation
Generates two-person comedy skits from product ideas.
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { generateSkitScript } from '@/lib/drama/skit-generator';
export async function POST(req: NextRequest) {
const session = await getServerSession();
if (!session?.user) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { productIdea, language } = await req.json();
const script = await generateSkitScript({
productIdea,
language,
atlasApiKey: process.env.ATLASCLOUD_API_KEY!,
});
const characterA = await generateCharacterVideo(script.characterA);
const characterB = await generateCharacterVideo(script.);
.({
script,
: (characterA, characterB, script.),
});
}
Credit System
Video Pricing Calculation
const CREDIT_USD = 0.01;
const ACCOUNT_MARKUP = 1.0;
const MARGIN = 1.2;
const PER_SECOND_RATES = {
'480p': 0.005,
'720p': 0.01,
'1080p': 0.02,
};
export function calculateVideoCredits({
durationSeconds,
resolution = '720p',
model = 'seedance-2.0',
}: {
durationSeconds: number;
resolution?: '480p' | '720p' | '1080p';
model?: string;
}): number {
const baseRate = PER_SECOND_RATES[resolution];
const credits = Math.ceil(
baseRate * durationSeconds * ACCOUNT_MARKUP * MARGIN / CREDIT_USD
);
return credits;
}
export const FIXED_CREDITS = {
imageGeneration: 10,
scriptGeneration: 5,
tts: 3,
: ,
};
Deducting Credits
import { prisma } from '@/lib/db';
export async function deductCredits(
userId: string,
amount: number,
description: string
) {
const user = await prisma.user.findUnique({ where: { id: userId } });
if (!user || user.credits < amount) {
throw new Error('Insufficient credits');
}
await prisma.user.update({
where: { id: userId },
data: { credits: { decrement: amount } },
});
await prisma.creditTransaction.create({
data: {
userId,
amount: -amount,
description,
type: 'DEDUCTION',
},
});
}
Atlas Cloud Integration
Main Client Setup
export class AtlasCloudClient {
private apiKey: string;
private baseUrl = 'https://api.atlascloud.ai/v1';
constructor(apiKey: string) {
this.apiKey = apiKey;
}
async generateImage({
prompt,
model = 'nano-banana/edit',
width = 1024,
height = 1024,
}: {
prompt: string;
model?: string;
width?: number;
height?: number;
}) {
const response = await fetch(`${this.baseUrl}/image/generate`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ prompt, model, width, height }),
});
return await response.json();
}
async generateVideo({
imageUrl,
prompt,
duration = ,
model = ,
}: {
imageUrl: ;
prompt: ;
duration?: ;
model?: ;
}) {
response = (, {
: ,
: {
: ,
: ,
},
: .({
model,
: imageUrl,
prompt,
duration,
}),
});
response.();
}
() {
response = (, {
: ,
: {
: ,
: ,
},
: .({
: videoUrl,
: audioUrl,
}),
});
response.();
}
}
Using the Client
import { AtlasCloudClient } from '@/lib/atlas';
const atlas = new AtlasCloudClient(process.env.ATLASCLOUD_API_KEY!);
const firstFrame = await atlas.generateImage({
prompt: `Product photo: ${productName} with presenter`,
model: 'nano-banana/edit',
});
const video = await atlas.generateVideo({
imageUrl: firstFrame.imageUrl,
prompt: 'Presenter enthusiastically presenting product',
duration: 15,
model: 'seedance-2.0',
});
const audio = await atlas.generateTTS({
text: scriptText,
voice: 'en-US-Standard-A',
});
const finalVideo = await atlas.lipSync({
videoUrl: video.videoUrl,
audioUrl: audio.audioUrl,
});
Payment Integration
Stripe Checkout
import Stripe from 'stripe';
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
apiVersion: '2023-10-16',
});
export async function createCheckoutSession({
userId,
creditPackId,
successUrl,
cancelUrl,
}: {
userId: string;
creditPackId: string;
successUrl: string;
cancelUrl: string;
}) {
const pack = CREDIT_PACKS[creditPackId];
const session = await stripe.checkout.sessions.create({
mode: 'payment',
line_items: [
{
price_data: {
currency: 'usd',
product_data: {
name: pack.name,
description: `${pack.credits} video generation credits`,
},
unit_amount: pack.priceUSD * 100,
},
quantity: 1,
},
],
success_url: successUrl,
cancel_url: cancelUrl,
metadata: {
userId,
creditPackId,
: pack.,
},
});
session;
}
Redeem Code System
import { NextRequest, NextResponse } from 'next/server';
import { getServerSession } from 'next-auth';
import { prisma } from '@/lib/db';
export async function POST(req: NextRequest) {
const session = await getServerSession();
if (!session?.user?.id) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
}
const { code } = await req.json();
const redeemCode = await prisma.redeemCode.findUnique({
where: { code },
});
if (!redeemCode || redeemCode.used || redeemCode.expiresAt < new Date()) {
return NextResponse.json({ error: 'Invalid or expired code' }, { status: });
}
prisma..({
: { : redeemCode. },
: {
: ,
: (),
: session..,
},
});
prisma..({
: { : session.. },
: {
: { : redeemCode. },
},
});
.({ : redeemCode. });
}
Database Schema
// prisma/schema.prisma
model User {
id String @id @default(cuid())
email String @unique
name String?
image String?
credits Int @default(100)
accounts Account[]
sessions Session[]
creations Creation[]
transactions CreditTransaction[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Creation {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
type String // 'ugc-ad', 'reference-ad', 'drama-ad', 'ad-skit'
status String // 'processing', 'completed', 'failed'
inputData String // JSON blob
outputData String? // JSON blob
videoUrl String?
creditsUsed Int
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model CreditTransaction {
id String @id @default(cuid())
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
amount Int
type String // 'PURCHASE', 'DEDUCTION', 'REFUND', 'REDEEM'
description String
createdAt DateTime @default(now())
}
model RedeemCode {
id String @id @default(cuid())
code String @unique
credits Int
used Boolean @default(false)
usedAt DateTime?
usedBy String?
expiresAt DateTime
createdAt DateTime @default(now())
}
Configuration
Pricing Configuration
export const CREDIT_PACKS = {
starter: {
id: 'starter',
name: 'Starter Pack',
credits: 500,
priceUSD: 5,
bonusPercent: 0,
},
pro: {
id: 'pro',
name: 'Pro Pack',
credits: 2000,
priceUSD: 18,
bonusPercent: 10,
},
business: {
id: 'business',
name: 'Business Pack',
credits: 5000,
priceUSD: 40,
bonusPercent: 20,
},
};
Platform Adapters
import { PrismaClient } from '@prisma/client';
const globalForPrisma = global as unknown as { prisma: PrismaClient };
export const prisma =
globalForPrisma.prisma ||
new PrismaClient({
log: process.env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'],
});
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = prisma;
Media Storage
Upload Pattern
export async function uploadImage(file: File): Promise<string> {
if (process.env.VERCEL) {
const { put } = await import('@vercel/blob');
const blob = await put(file.name, file, {
access: 'public',
token: process.env.BLOB_READ_WRITE_TOKEN,
});
return blob.url;
} else {
const key = `uploads/${Date.now()}-${file.name}`;
const arrayBuffer = await file.arrayBuffer();
await env.MEDIA_BUCKET.put(key, arrayBuffer, {
httpMetadata: {
contentType: file.type,
},
});
return `${env.R2_PUBLIC_URL}/${key}`;
}
}
Common Patterns
Workflow with Credit Check
export async function generateWithCreditsCheck({
userId,
workflowFn,
estimatedCredits,
}: {
userId: string;
workflowFn: () => Promise<{ videoUrl: string; creditsUsed: number }>;
estimatedCredits: number;
}) {
const user = await prisma.user.findUnique({ where: { id: userId } });
if (!user || user.credits < estimatedCredits) {
throw new Error('Insufficient credits');
}
const creation = await prisma.creation.create({
data: {
userId,
type: 'ugc-ad',
status: 'processing',
inputData: JSON.stringify({}),
creditsUsed: 0,
},
});
try {
const result = await workflowFn();
await deductCredits(userId, result.creditsUsed, 'UGC Ad Generation');
prisma..({
: { : creation. },
: {
: ,
: result.,
: result.,
},
});
result;
} (error) {
prisma..({
: { : creation. },
: { : },
});
error;
}
}
Multi-Language Script Generation
export async function generateAdScript({
productName,
targetAudience,
tone,
language = 'en',
atlasApiKey,
}: {
productName: string;
targetAudience: string;
tone: string;
language?: string;
atlasApiKey: string;
}) {
const prompt = `Generate a ${tone} UGC ad script for "${productName}" targeting ${targetAudience}.
Language: ${language}.
Format: 15-second spoken script, first-person perspective.`;
const response = await fetch('https://api.atlascloud.ai/v1/llm/generate', {
method: 'POST',
headers: {
'Authorization': `Bearer ${atlasApiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'gpt-4o-mini',
prompt,
maxTokens: 200,
}),
});
const result = await response.json();
return result.text;
}
Troubleshooting
Issue: "Insufficient credits" during generation
Solution: Check actual credit balance and increase estimated credits buffer:
const estimatedCredits = calculateVideoCredits({
durationSeconds: 15,
resolution: '720p',
}) * 1.2;
Issue: Atlas Cloud API timeout
Solution: Implement retry logic with exponential backoff:
async function callAtlasWithRetry(fn: () => Promise<any>, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (i === maxRetries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, 1000 * Math.pow(2, i)));
}
}
}
Issue: Video URL not accessible by Atlas Cloud
Solution: Ensure media storage is publicly accessible:
const signedUrl = await env.MEDIA_BUCKET.createSignedUrl(key, {
expiresIn: 3600,
});
const blob = await put(file.name, file, {
access: 'public',
});
Issue: Prisma schema mismatch after deployment
Solution: Push schema changes before deploying:
npm run db:push:vercel
vercel deploy
npm run db:push:cf
npm run cf:deploy
Issue: NextAuth session not persisting on Cloudflare
Solution: Verify environment variables and session adapter:
export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(prisma),
session: {
strategy: 'database',
maxAge: 30 * 24 * 60 * 60,
},
};