diff --git a/.claude/settings.local.json b/.claude/settings.local.json index e1881b2..56cda1a 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -8,7 +8,47 @@ "WebSearch", "WebFetch(domain:github.com)", "WebFetch(domain:developer.nextdoor.com)", - "WebFetch(domain:docs.postiz.com)" + "WebFetch(domain:docs.postiz.com)", + "Bash(npm install:*)", + "Bash(npx next:*)", + "Bash(curl -s http://localhost:3000/api/auth/session)", + "Bash(curl -s http://localhost:3000/login)", + "Bash(curl -s http://localhost:3000/ -L)", + "Bash(curl -s http://localhost:3000/api/campaigns)", + "Bash(curl -s -o /dev/null -w \"Status: %{http_code}\" http://localhost:3000/)", + "Bash(curl -s -o /dev/null -w \"%{http_code}\" -b /tmp/cookies3.txt \"http://localhost:3000/\")", + "Bash(curl -s -o /dev/null -w \"%{http_code}\" -b /tmp/cookies3.txt \"http://localhost:3000/campaigns\")", + "Bash(curl -s -o /dev/null -w \"%{http_code}\" -b /tmp/cookies3.txt \"http://localhost:3000/campaigns/new\")", + "Bash(curl -s -o /dev/null -w \"%{http_code}\" -b /tmp/cookies3.txt \"http://localhost:3000/assets\")", + "Bash(curl -s -o /dev/null -w \"%{http_code}\" -b /tmp/cookies3.txt \"http://localhost:3000/trends\")", + "WebFetch(domain:www.npmjs.com)", + "WebFetch(domain:raw.githubusercontent.com)", + "Bash(curl:*)", + "Bash(sqlite3:*)", + "Bash(ls:*)", + "Bash(OUTDIR=\"../outputs/task_management_feature_launch_20260323/video\" __NEW_LINE_74c5312ec027b42b__ npx remotion render src/index.ts Gemini-IG-Feed-Cost --output \"$OUTDIR/video_gemini_ig_feed_cost_1080x1920.mp4\")", + "Bash(npx remotion:*)", + "Bash(npx tsx -e \":*)", + "Bash(OUTDIR=\"../outputs/task_management_feature_launch_20260323/video\")", + "Bash(npx tsx:*)", + "Bash(open:*)", + "Bash(git add:*)", + "Bash(git commit:*)", + "Bash(git checkout:*)", + "Bash(npm run:*)", + "Bash(mkdir -p /Users/treyt/Desktop/code/claude_marketing/pipeline/apps/reflect/screenshots)", + "Bash(cp /Users/treyt/Downloads/flow_03_q1.png /Users/treyt/Downloads/flow_05_q3_scrolled.png /Users/treyt/Downloads/flow_06_q4.png /Users/treyt/Downloads/flow_07_detail_after.png /Users/treyt/Downloads/flow_q2_typing.png /Users/treyt/Desktop/code/claude_marketing/pipeline/apps/reflect/screenshots/)", + "Bash(grep -v \"^$\")", + "Bash(find /Users/treyt/Desktop/code/claude_marketing/pipeline/outputs/reflect_v1.1_—_guided_reflection_ _ai_reports_20260325/ -type f \\\\\\(-name *.png -o -name *.mp4 \\\\\\))", + "Read(//tmp/**)", + "Bash(ssh unraid:*)", + "Bash(scp:*)", + "Bash(rsync:*)", + "Bash(openssl rand:*)", + "Bash(security find-generic-password:*)", + "Bash(claude auth:*)", + "Bash(claude setup-token:*)", + "Bash(dig marketing.88oakapps.com +short)" ] } } diff --git a/AI_UGC_VIDEO_PLATFORMS_RESEARCH_2026.md b/AI_UGC_VIDEO_PLATFORMS_RESEARCH_2026.md new file mode 100644 index 0000000..7dcd25b --- /dev/null +++ b/AI_UGC_VIDEO_PLATFORMS_RESEARCH_2026.md @@ -0,0 +1,762 @@ +# AI UGC Video Generation Platforms Research 2025-2026 +## Realistic "Person Using Phone" Lifestyle Video Analysis + +**Research Date**: March 2026 +**Focus**: Platforms for realistic video clips of people naturally interacting with phones/tablets (NOT talking-head testimonials) + +--- + +## EXECUTIVE SUMMARY + +For your specific use case—realistic lifestyle videos of people naturally using apps on phones (checking mood apps, couples looking at screens, tapping before bed, showing phones to family)—**the landscape is fragmented**: + +- **Text-to-video models** (Runway, Kling, Google Veo, Sora) can generate general "person using phone" scenarios from text prompts but require careful prompt engineering +- **Avatar platforms** (HeyGen, Synthesia, D-ID) excel at talking-head presenters, NOT lifestyle interaction videos +- **Specialized UGC platforms** (MakeUGC, Creatify, Arcads) can make realistic people holding products but have limited "phone interaction" capabilities +- **Phone mockup tools** (Mockey, Rotato, FlexClip) handle app screen display but lack realistic human actors + +**Best Match for Your Use Case**: A combination approach using Runway Gen-4.5 or Google Veo 3.1 for lifestyle generation + a phone mockup tool for screen display integration. + +--- + +## DETAILED PLATFORM ANALYSIS + +### 1. RUNWAY GEN-4 / GEN-4.5 + +**Phone Interaction Capability**: ⭐⭐⭐⭐⭐ (Excellent) +**API Access**: ⭐⭐⭐⭐⭐ (Yes, fully supported) +**Diverse Cast**: ⭐⭐⭐⭐ (Via detailed prompts) +**Overall Fit**: ⭐⭐⭐⭐⭐ (BEST OPTION for general "person using phone" videos) + +**What It Does Well**: +- **Character & Scene Consistency**: Gen-4 maintains consistent characters across multiple shots +- **Physics Simulation**: Realistic weight, momentum, motion—crucial for natural phone interactions +- **Camera Control**: Advanced camera movements (zoom, arc, trucking) +- **Gen-4.5 Performance**: Released December 2025, now #1 on Artificial Analysis Text-to-Video benchmark with 1,247 Elo points + +**Can It Do Your Use Cases?** +- ✅ Person checking phone at breakfast and smiling +- ✅ Couple looking at phone together on couch (with proper prompting) +- ✅ Someone tapping phone quickly before bed +- ✅ Parent showing teen something on phone + +**API Details**: +- Native API with modern documentation +- Generation speed: 5-8 second videos in ~60 seconds (5x faster than Gen-4) +- Supports text-to-video and image-to-video +- Available via Runway's official API + +**Pricing**: +- No official per-video pricing published +- Credit-based system through third-party APIs (CometAPI, AIML API, etc.) +- Estimated: $0.25-$0.50 per 8-second video through aggregator APIs +- Enterprise/volume discounts available + +**Node.js/TypeScript Integration**: +- Native Node.js SDK available: `npm install @runwayml/sdk` +- REST API with standard authentication +- Can be integrated into automated pipelines + +**Quality**: Extremely high—bleeding-edge photorealism, best for lifestyle sequences + +--- + +### 2. GOOGLE VEO 3 / VEO 3.1 + +**Phone Interaction Capability**: ⭐⭐⭐⭐⭐ (Excellent) +**API Access**: ⭐⭐⭐⭐ (Yes, via Gemini API) +**Diverse Cast**: ⭐⭐⭐⭐ (Better with reference images) +**Overall Fit**: ⭐⭐⭐⭐⭐ (EXCELLENT, comparable to Runway) + +**What It Does Well**: +- **Native Audio Generation**: Generates synchronized audio alongside video +- **Human Face Generation**: Veo 3.1 can generate realistic human faces when provided references (advantage over Sora) +- **Image-to-Video**: Enhanced capabilities for maintaining character consistency +- **October 2025 Release**: Latest production model with high-fidelity outputs + +**Can It Do Your Use Cases?** +- ✅ All four use cases similar to Runway, with added audio sync +- ✅ Better for complex scenes with multiple people (family showing scenarios) + +**API Details**: +- Available via Gemini API (Google's unified API) +- Pricing available on Vertex AI platform +- Can integrate with Google Cloud Platform workflows + +**Pricing**: +- Vertex AI: $0.40 per second (standard), $0.15 per second (faster model) +- For 30-second video: ~$12 (standard) or ~$4.50 (faster) +- Gemini API: Different pricing tier (check latest) +- Free preview tier available for experimentation + +**Node.js/TypeScript Integration**: +- Google Cloud Node.js client libraries available +- Standard REST API access +- Integrates with existing GCP infrastructure + +**Quality**: Very high, with better audio sync than Runway. Strong for family/couple scenarios. + +--- + +### 3. SORA (OPENAI) + +**Phone Interaction Capability**: ⭐⭐⭐⭐ (Very Good) +**API Access**: ⭐⭐ (NOT AVAILABLE - major limitation) +**Diverse Cast**: ⭐⭐⭐ (Possible with prompts) +**Overall Fit**: ⭐⭐ (NOT SUITABLE for automation) + +**Status**: +- Sora 2 released September 2025 +- **No public API** as of January 2026 +- WaveSpeedAI offers unofficial Sora 2 API access (not directly supported by OpenAI) +- January 2026 change: Free users can no longer generate—Plus ($20/mo) and Pro ($200/mo) only + +**Capabilities**: +- Can generate professional-quality videos up to 25 seconds with synchronized dialogue +- More "physically accurate and realistic" than earlier models +- Can handle complex human interactions + +**Why Not Suitable**: +- No direct API access from OpenAI +- Relies on web app or unofficial third-party APIs +- Can't be directly integrated into automated pipelines +- Subscription-locked (no free tier) + +**Recommendation**: Skip for your automation needs. + +--- + +### 4. KLING AI 3.0 + +**Phone Interaction Capability**: ⭐⭐⭐⭐ (Very Good) +**API Access**: ⭐⭐⭐⭐ (Yes, via multiple providers) +**Diverse Cast**: ⭐⭐⭐⭐ (Strong) +**Overall Fit**: ⭐⭐⭐⭐ (GOOD alternative to Runway/Veo) + +**What It Does Well**: +- **Physics Accuracy**: Simulates gravity, balance, inertia for believable movement +- **Face Stability**: Characters remain consistent across frames (February 2026 launch solved this major pain point) +- **Element Library**: Upload reference images to ensure characters stay consistent across shots +- **Audio Sync**: Native audio with video for up to 5 minutes + +**Can It Do Your Use Cases?** +- ✅ Person checking phone at breakfast +- ✅ Couple looking at phone together +- ✅ Tapping phone before bed +- ✅ Parent/teen scenarios (with reference images for consistency) + +**Kling 3.0 Specifics** (Unified multimodal video engine): +- Cinema-grade visuals +- Physics-accurate motion +- Native audio sync +- Released February 2026 + +**API Access**: +- Multiple third-party providers: fal.ai, Runware, WaveSpeedAI, PiAPI +- Element Library feature available for character consistency +- Supports text-to-video and image-to-video + +**Pricing**: +- Variable by provider, but generally affordable (cheaper than Runway/Veo) +- fal.ai: Pay-per-use model (check current rates) +- Estimated: $0.10-$0.30 per video through aggregators + +**Node.js/TypeScript Integration**: +- Available through fal.ai SDK (`npm install @fal-ai/client`) +- REST API through aggregator platforms +- Straightforward integration + +**Quality**: Very high, especially after 3.0 launch. Excellent value for cost. + +--- + +### 5. HEYGEN (Avatar-Based) + +**Phone Interaction Capability**: ⭐⭐ (Limited) +**API Access**: ⭐⭐⭐⭐⭐ (Excellent) +**Diverse Cast**: ⭐⭐⭐ (100+ avatars available) +**Overall Fit**: ⭐⭐ (NOT IDEAL - focused on talking heads) + +**Problem**: HeyGen specializes in **avatar presenters speaking to camera**, NOT lifestyle interactions. + +**Latest Features (February 2026)**: +- Avatar IV with motion-captured avatars +- Timing-aware hand gestures +- Micro-expressions (natural blinks, subtle smiles) +- Redesigned homepage +- ChatGPT integration +- Video Agent API (new) + +**Avatar IV Performance**: +- Full-body avatars with realistic lip-sync +- Hand gesture timing +- Micro-expressions +- Digital Twin feature (create version of yourself) + +**When It Might Work**: +- Could potentially show avatar using phone in script, but very artificial +- Better for product explainers where avatar talks about the app + +**API Details**: +- Video Agent API: prompt-to-video workflows +- REST API with Node.js support +- Multiple video generation, translation, LiveAvatar streaming endpoints + +**Pricing**: +- API starts at $99/month +- Credit-based: 1 credit = 1 minute avatar video (standard) +- Avatar IV uses 1 credit per 10 seconds (~6 credits/minute) +- Video Agent: ~2 credits per minute +- Translation: 3 credits per minute of source video +- Pro tier: $0.99/credit, Scale tier: $0.50/credit + +**Recommendation**: Use only if you want talking-head explainer videos about the app, NOT lifestyle interaction videos. + +--- + +### 6. SYNTHESIA (Avatar-Based) + +**Phone Interaction Capability**: ⭐⭐ (Limited) +**API Access**: ⭐⭐⭐ (Yes, Creator plan+) +**Diverse Cast**: ⭐⭐⭐⭐ (160+ avatars, real actors) +**Overall Fit**: ⭐⭐ (NOT IDEAL - talking head focused) + +**What It Does**: +- Express-2 engine: full-body avatars with gestures, pointing, waving +- All avatars based on real actors (paid consent model) +- Facial micro-expressions matching emotional tone +- 160+ languages supported + +**API Access**: +- Creator plan: $64/month (billed yearly, $18/month equivalent) +- Includes API access with rate limits +- Webhook integration for automated workflows + +**Pricing**: +- **Free**: 36 minutes/year +- **Starter**: $18/month (annual) = ~0.33 credits/minute +- **Creator**: $64/month (annual) - includes API +- **Enterprise**: Custom pricing +- Credit system: 1 minute = 1 credit + +**Node.js/TypeScript Integration**: +- REST API with Node.js support +- Webhook integration for async workflows +- Standard authentication + +**Why Not Ideal**: +- Designed for presenters/training videos, not lifestyle interaction +- Avatars still feel "presenter-like" rather than casual interaction +- Better for corporate than authentic UGC + +**Recommendation**: Skip for this use case. + +--- + +### 7. PIKA LABS 2.2 + +**Phone Interaction Capability**: ⭐⭐⭐⭐ (Good) +**API Access**: ⭐⭐⭐⭐ (Yes, via fal.ai) +**Diverse Cast**: ⭐⭐⭐ (Text-prompt based) +**Overall Fit**: ⭐⭐⭐ (Decent alternative) + +**What It Does**: +- Text-to-video generation (Pika 2.2) +- Image-to-video (Pikascenes 2.2) +- Pikaframes 2.2: upload 5 keyframes, AI interpolates smooth motion +- Pikaformance: hyper-real expressions synced to audio (near real-time) + +**API Access**: +- December 2025 announcement: Pika 2.2 now exposed via fal.ai +- API key through fal dashboard +- Text-to-video and image-to-video endpoints + +**Use Cases**: +- ✅ Can generate "person using phone" via text prompts +- ✅ Pikaframes could help create consistent character across shots +- Less ideal than Runway/Veo for this specific use case + +**Pricing**: Not clearly published; likely variable through fal.ai aggregator + +**Quality**: Good, but less consistent character realism than Runway Gen-4.5 + +--- + +### 8. D-ID (Real-Time Avatar Video) + +**Phone Interaction Capability**: ⭐⭐ (Limited) +**API Access**: ⭐⭐⭐⭐⭐ (Excellent - core product) +**Diverse Cast**: ⭐⭐ (Limited to avatar variations) +**Overall Fit**: ⭐⭐ (NOT suitable) + +**New V4 Expressive Visual Agents (March 2026)**: +- Ultra-high-fidelity digital humans +- Real-time LLM-connected conversations +- Sub-0.5-second latency +- Up to 4K resolution +- Sentiment-aligned facial expressions +- Trained on real actor performances + +**Best Use Case**: +- Customer support chatbots with realistic avatars +- Interactive training experiences +- NOT lifestyle video content + +**Why Not Suitable**: +- Designed for talking-head interactions +- Real-time conversational focus +- Not for pre-recorded lifestyle scenarios + +**Recommendation**: Skip for your use case. + +--- + +### 9. TAVUS (Real-Time AI Humans) + +**Phone Interaction Capability**: ⭐⭐⭐ (Moderate) +**API Access**: ⭐⭐⭐⭐ (Yes, with real-time capability) +**Diverse Cast**: ⭐⭐ (Requires custom avatar creation) +**Overall Fit**: ⭐⭐⭐ (Possible but expensive) + +**What It Does**: +- Creates hyperrealistic AI replicas from 2-minute video sample +- Phoenix-4 model: first real-time model with emotional states + active listening +- Emotional states, facial expressions, head movements as unified system +- Millisecond-level latency + +**Pricing**: +- Free plan: 25 min/month conversational, 5 min/month generation ($0 cost) +- Starter: ~$39-59/month +- Growth: 1,250 min/month conversational +- Overage: $0.37/min conversations, $0.32/min overage (Growth tier) +- Enterprise: Custom (resource-intensive, expensive) + +**Use Cases**: +- ✅ Could generate video of person using phone if you create custom avatar +- ✅ Real-time interaction capability (not needed for your use case) +- ❌ Expensive for batch video generation + +**Why Less Ideal**: +- Designed for real-time conversational avatars +- Creating custom avatars is expensive +- Better for interactive experiences than pre-recorded lifestyle videos + +--- + +### 10. MAKEUGC (Specialized UGC Platform) + +**Phone Interaction Capability**: ⭐⭐⭐ (Moderate) +**API Access**: ⭐⭐⭐⭐ (Yes, Platform API) +**Diverse Cast**: ⭐⭐⭐⭐ (100+ licensed AI avatars) +**Overall Fit**: ⭐⭐⭐ (GOOD for avatar-based content) + +**What It Does**: +- 100+ unique licensed AI avatars +- Avatar can realistically hold/showcase/consume products +- Testimonial and lifestyle shot generation +- Text script → AI avatar video transformation + +**Key Feature**: +- Proprietary hand-holding technology: avatars can realistically hold products +- Could potentially adapt for "holding phone" scenarios + +**API Details**: +- Platform API for programmatic video generation +- Authentication via API key +- Specify avatar, voice, script +- Processing time: 2-10 minutes for talking head videos +- 29 languages supported + +**Pricing**: +- Under $10 per video (mentioned as cost comparison to $100-200 traditional UGC) +- Subscription required (exact tiers unclear from search) + +**Node.js/TypeScript Integration**: +- REST API should be straightforward to integrate +- Check documentation at app.makeugc.ai/api/platform/documentation + +**Use Cases**: +- ✅ Person holding phone showing it to others (good fit) +- ✅ Product holding = could adapt for phone +- ❌ More formal/structured than casual lifestyle +- ❌ Feels more like testimonial than authentic interaction + +**Quality**: Good for product-focused UGC, less natural for casual lifestyle scenarios + +--- + +### 11. CREATIFY + +**Phone Interaction Capability**: ⭐⭐⭐ (Moderate) +**API Access**: ⭐⭐⭐⭐ (Yes, Business plan+) +**Diverse Cast**: ⭐⭐⭐⭐ (1500+ hyper-realistic UGC avatars) +**Overall Fit**: ⭐⭐⭐ (GOOD for avatar-based UGC) + +**What It Does**: +- 1500+ hyper-realistic UGC avatars +- Aurora avatar model (state-of-the-art) +- Text-to-video, URL-to-video, image-to-video +- Custom templates, product videos, AI Shorts + +**API Capabilities**: +- URL-to-video conversion +- AI avatar lip-sync +- Aurora image-to-video +- Custom templates +- Text-to-Speech + +**Pricing**: +- Free: 10 credits (≈2 videos) +- Creator: $39/month (annual) or $33/month annual = 50 credits/month +- Business: $99/month = 250 credits/month + API access + priority support +- Enterprise: Custom with volume discounts +- Credit cost: 2-20 per video depending on quality + +**Estimated Cost**: +- At Business tier: $99/250 credits = ~$0.40/credit +- 10-credit video = ~$4, 20-credit video = ~$8 + +**Node.js/TypeScript Integration**: +- REST API on Business plan +- Check docs.creatify.ai for API details + +**Use Cases**: +- ✅ Person holding/showing phone +- ✅ Family/couple scenarios with different avatars +- ✅ Good diversity in avatar library +- ❌ May feel more "production" than authentic UGC + +--- + +### 12. ARCADS.AI (Specialized UGC) + +**Phone Interaction Capability**: ⭐⭐ (Limited) +**API Access**: ⭐⭐⭐⭐ (Yes, Enterprise+) +**Diverse Cast**: ⭐⭐⭐⭐ (300+ actors from video footage) +**Overall Fit**: ⭐⭐⭐ (Possible but not ideal) + +**What It Does**: +- 300+ AI "actors" from real video footage (better body language than synthetic) +- TikTok-style UGC video ads +- Avatars can hold products and show apps +- B-rolls, music, captions, transitions auto-added + +**Can They Do Phone?** +- ✅ Can make avatar hold phone and show app +- ❌ Struggles with physical products, likely limited for realistic phone interaction + +**API Details**: +- Enterprise plans include API access +- Trigger generation from briefs +- Auto-route to cloud storage + +**Pricing**: +- Starter: $110/month = 10 videos/month = $11/video +- Creator: $220/month = 20 videos/month = $11/video +- Custom plans for volume + API access + +**Why Less Ideal**: +- Platform struggles with physical product interactions +- More TikTok-ad focused than lifestyle +- Enterprise-only API (high minimum commitment) + +--- + +## PHONE MOCKUP / APP SCREEN DISPLAY TOOLS + +If you need to show actual phone screens, these complement AI video tools: + +### Mockey.ai +- Phone mockup video generator +- Add your design, generate MP4 mockup +- Templates with realistic person holding phone +- Good for app screen display + +### Rotato +- 3D device mockups +- Your own app/web designs on device screens +- High-quality visuals + +### FlexClip +- Free phone mockup generator +- Display app screenshots on iPhone/Android backgrounds +- AI image tools (object remover, voice generator) +- Integrated with video editor + +### Placeit (by Envato) +- App mockup templates +- Animated device displays +- Professional quality + +**Strategy**: Use AI video generator for realistic people, combine with mockup tool for accurate phone screen display. + +--- + +## RECOMMENDATION MATRIX + +### For Your Specific Use Cases: + +**Use Case: "Person checking phone at breakfast and smiling"** +- **Best**: Runway Gen-4.5 with detailed prompt +- **Alternative**: Google Veo 3.1 +- **Budget**: Kling AI 3.0 + +**Use Case: "Couple looking at phone together on couch"** +- **Best**: Runway Gen-4.5 (multi-character consistency) +- **Alternative**: Google Veo 3.1 +- **Budget**: Kling AI 3.0 + +**Use Case: "Someone tapping phone quickly before bed"** +- **Best**: Runway Gen-4.5 (motion capture precision) +- **Alternative**: Kling AI 3.0 (physics simulation) + +**Use Case: "Parent showing teen something on phone"** +- **Best**: Runway Gen-4.5 or Google Veo 3.1 (multi-person interaction) +- **Alternative**: MakeUGC or Creatify (controlled avatar setup) + +--- + +## IMPLEMENTATION ARCHITECTURE + +### Option A: Text-to-Video Foundation (Recommended) + +```typescript +// Runway Gen-4.5 approach +const prompt = ` +A woman sits at her kitchen table with breakfast, +holding her phone. She glances at it, reads something +that makes her smile. Natural morning lighting. Shot +from medium distance, gentle camera movement. +`; + +// Generate via Runway API +const video = await runwayClient.generateVideo({ + prompt, + duration: 10, + quality: 'high' +}); +``` + +**Pros**: +- Single source of truth +- High realism +- Character consistency +- Flexible scenarios + +**Cons**: +- Phone screen not visible +- Prompt engineering required +- May need multiple generations for variations + +### Option B: Composite Approach + +```typescript +// Generate person using phone video +const personVideo = await runwayClient.generateVideo({ + prompt: "Woman checking her phone at breakfast, smiling", + duration: 10 +}); + +// Create phone mockup with your actual app UI +const phoneVideo = await mockeyClient.generateMockup({ + appScreenshot: moodAppScreenshot, + template: 'hand_holding_phone' +}); + +// Composite them together (requires video editing) +const final = compositeVideos(personVideo, phoneVideo); +``` + +**Pros**: +- Shows actual app UI +- Customizable +- Control over phone screen content + +**Cons**: +- Requires video compositing +- More complex pipeline +- Phone screen doesn't match hand/phone position perfectly + +### Option C: UGC Avatar Platform + +```typescript +// Creatify approach - controlled but less flexible +const video = await creatifyClient.generateVideo({ + avatarId: 'avatar_diverse_female_30s', + script: 'Let me show you our mood tracking app', + voiceId: 'natural_female_voice', + backgroundTemplate: 'modern_bedroom', + productUrl: 'https://yourapp.com' +}); +``` + +**Pros**: +- Controlled, consistent output +- Diverse avatars available +- Quick generation + +**Cons**: +- Less natural/authentic +- Limited "lifestyle" feel +- Feels more like testimonial + +--- + +## FINAL RECOMMENDATION FOR YOUR PIPELINE + +### Best Solution: **Runway Gen-4.5 + Optional Compositing** + +**Why**: +1. **Highest Quality**: #1 on AI video benchmarks +2. **API First**: Built for automation, excellent Node.js integration +3. **Handles All Use Cases**: Can generate realistic multi-person interactions, natural gestures, emotional micro-expressions +4. **Reasonable Pricing**: ~$0.25-$0.50 per 8-10 second video (through aggregators) +5. **Character Consistency**: Maintains same person across shots and variations + +**Integration Path**: + +```typescript +import Anthropic from "@anthropic-sdk/sdk"; +import Runway from "@runwayml/sdk"; + +const runway = new Runway({ + apiKey: process.env.RUNWAY_API_KEY +}); + +async function generateMoodAppUGC(scenario: string) { + const prompt = ` + Realistic, natural lighting. Shot composition appropriate for the scenario. + ${scenario} + + Character: diverse, relatable person + Style: authentic UGC, not staged/commercial + Duration: 8-10 seconds + `; + + const video = await runway.generateVideo({ + prompt, + duration: 10, + aspectRatio: "9:16" // TikTok/Instagram vertical + }); + + return video; +} + +// Generate variations +const scenarios = [ + "Woman checking her phone at breakfast, sees notification, smiles", + "Couple sitting on couch, passing phone back and forth, both smiling", + "Teenager in bedroom, taps phone quickly before sleeping", + "Parent showing child phone screen, both looking engaged" +]; + +for (const scenario of scenarios) { + const video = await generateMoodAppUGC(scenario); + await saveVideo(video); +} +``` + +**Estimated Pipeline Costs**: +- 4 videos × $0.35 average = $1.40 +- 100 videos/month = $35 +- 1,000 videos/month = $350 (scale pricing may apply) + +### Secondary Option: **Google Veo 3.1** + +If you prefer: +- Native audio sync in videos +- More conservative, "safe" generation +- Integrated Google Cloud infrastructure +- Reference image consistency for characters + +**Cost**: $0.40/second standard = ~$4 per 10-second video + +### Budget Option: **Kling AI 3.0** + +If you're price-sensitive: +- ~$0.10-$0.30 per video +- Still excellent quality (especially Kling 3.0) +- Good physics for natural gestures +- Element Library for character consistency + +--- + +## NODE.JS IMPLEMENTATION CHECKLIST + +- [ ] Install Runway SDK or use their REST API +- [ ] Set up authentication (API keys in environment) +- [ ] Create prompt templates for each UGC scenario +- [ ] Implement video generation with error handling/retries +- [ ] Set up webhook/polling for async generation +- [ ] Download and organize generated videos +- [ ] (Optional) Integrate video compositing library for phone screen mockups +- [ ] Create variation generator (prompt templates with parameters) +- [ ] Implement quality/consistency checks +- [ ] Log all API calls, costs, and video metadata + +--- + +## PLATFORMS TO AVOID FOR THIS USE CASE + +❌ **HeyGen**: Talking-head avatars, not lifestyle +❌ **Synthesia**: Corporate/training videos, not authentic UGC +❌ **D-ID**: Real-time chatbot avatars, not pre-recorded lifestyle +❌ **Tavus**: Expensive for batch generation, conversation-focused +❌ **Sora**: No public API, can't automate +❌ **Pika**: Good but less consistent character than Runway/Veo + +--- + +## KEY METRICS COMPARISON TABLE + +| Platform | Phone Interaction | API | Diverse Cast | API Cost/Video | Quality | Ease of Integration | +|----------|-------------------|-----|--------------|-----------------|---------|---------------------| +| **Runway Gen-4.5** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | $0.25-$0.50 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | +| **Google Veo 3.1** | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | $0.40/sec | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | +| **Kling AI 3.0** | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | $0.10-$0.30 | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | +| **Creatify** | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | $0.40-$8.00 | ⭐⭐⭐ | ⭐⭐⭐ | +| **MakeUGC** | ⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | <$10 | ⭐⭐⭐ | ⭐⭐⭐ | +| **Arcads** | ⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | $11 | ⭐⭐⭐ | ⭐⭐⭐ | +| **HeyGen** | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ | $0.50-$0.99 | ⭐⭐⭐ | ⭐⭐⭐⭐ | +| **Synthesia** | ⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐⭐ | $0.33-1.00 | ⭐⭐⭐ | ⭐⭐⭐ | + +--- + +## SOURCES + +### Primary Research Sources +- [Runway Gen-4 Research](https://runwayml.com/research/introducing-runway-gen-4) +- [Runway API Documentation](https://runwayml.com/api) +- [Google Veo 3.1 Announcement](https://developers.googleblog.com/introducing-veo-3-1-and-new-creative-capabilities-in-the-gemini-api/) +- [Google Veo API Docs](https://docs.cloud.google.com/vertex-ai/generative-ai/docs/model-reference/veo-video-generation) +- [Kling AI 3.0 Launch](https://higgsfield.ai/kling-o1-intro) +- [HeyGen API Pricing](https://www.heygen.com/api-pricing) +- [HeyGen February 2026 Release](https://www.heygen.com/blog/heygen-february-2026-release) +- [Synthesia API Docs](https://docs.synthesia.io/reference/introduction) +- [Synthesia Pricing 2026](https://www.synthesia.io/pricing) +- [D-ID V4 Announcement](https://www.d-id.com/news/v4-expressive-visual-agents-real-time-llm-connected-interaction/) +- [MakeUGC Platform API](https://app.makeugc.ai/api/platform/documentation) +- [Creatify API](https://creatify.ai/api) +- [Tavus Pricing](https://www.tavus.io/pricing) +- [Arcads AI Features](https://www.arcads.ai/features/) +- [Pika API via fal.ai](https://blog.fal.ai/pika-api-is-now-powered-by-fal) +- [AI Video Generation APIs 2025](https://www.tavus.io/post/high-quality-ai-video-api) +- [Best AI Video Generators 2026](https://zapier.com/blog/best-ai-video-generator/) + +--- + +## NEXT STEPS + +1. **Sign up for Runway API** with test credits +2. **Create prompt templates** for your 4 use cases +3. **Test generation** with various prompts and durations +4. **Measure quality** and iteration requirements +5. **Calculate actual costs** from real API usage +6. **Build Node.js pipeline** with error handling +7. **Implement variation system** (prompt parameters, style options) +8. **Monitor and optimize** prompts based on output quality + +--- + +**Last Updated**: March 2026 +**Research Methodology**: Comprehensive web search of 2025-2026 platform releases, API documentation, and pricing structures. diff --git a/CLAUDE.md b/CLAUDE.md index 43c994c..cecf7d7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1 +1,103 @@ @AGENTS.md + +# Marketing Command Center + +Next.js 16 app that uses Claude Code CLI as a subprocess to run an 8-agent marketing pipeline. Generates ads, videos, copy, and research for mobile apps. + +## Tech Stack +- Next.js 16.2.1, React 19, TypeScript 5, TailwindCSS 4, shadcn/ui +- Prisma 7.5 with SQLite (better-sqlite3 adapter) +- NextAuth 5 (credentials provider, `trustHost: true` required) +- Claude Code CLI spawned via `child_process.spawn()` (NOT the Anthropic SDK) +- Playwright for HTML-to-PNG rendering +- Remotion for video generation (in `pipeline/remotion-ad/`) + +## How Claude Is Used +The app spawns `claude -p --output-format stream-json` as a subprocess. Auth comes from the user's Claude Max subscription. In Docker, auth is via `CLAUDE_CODE_OAUTH_TOKEN` env var (an access token extracted from macOS Keychain `Claude Code-credentials` entry). See `lib/claude.ts` for all spawn logic. + +## Local Development +```bash +npm install +npx prisma db push +npx prisma db seed # creates admin user + honeyDue app +npm run dev # http://localhost:3000 +``` +Default login: `admin@localhost` / `admin123` (set via ADMIN_EMAIL/ADMIN_PASSWORD env vars). + +## Unraid Docker Deployment +The app runs on an Unraid server at `marketing.88oakapps.com` behind Nginx Proxy Manager. + +### Architecture +- App source: `/mnt/user/appdata/marketing/` (disposable, rsync from dev machine) +- Persistent data: `/mnt/user/downloads/marketing/` (survives app updates) + - `db/` - SQLite database + - `outputs/` - generated campaign assets (ads, videos, copy) + - `knowledge/` - brand identity, platform guidelines + +### Unraid docker-compose.yml (lives on Unraid, NOT the repo version) +The repo `docker-compose.yml` includes Postiz services for local dev. The Unraid version is app-only: +```yaml +services: + app: + build: . + ports: + - "3000:3000" + environment: + - NEXTAUTH_URL=http://localhost:3000 + - NEXTAUTH_SECRET=${NEXTAUTH_SECRET} + - ADMIN_EMAIL=${ADMIN_EMAIL:-admin@localhost} + - ADMIN_PASSWORD=${ADMIN_PASSWORD:-admin123} + - DATABASE_URL=file:./prisma/data/marketing.db + - PIPELINE_ROOT=/app/pipeline + - CLAUDE_CODE_OAUTH_TOKEN=${CLAUDE_CODE_OAUTH_TOKEN} + volumes: + - /mnt/user/downloads/marketing/db:/app/prisma/data + - /mnt/user/downloads/marketing/outputs:/app/pipeline/outputs + - /mnt/user/downloads/marketing/knowledge:/app/pipeline/knowledge +``` + +### Deploying Updates to Unraid +```bash +# Sync code (excludes env/compose so Unraid config isn't overwritten) +rsync -avz \ + --exclude='node_modules' --exclude='.next' --exclude='prisma/data' \ + --exclude='pipeline/outputs' --exclude='pipeline/remotion-ad/.next' \ + --exclude='pipeline/remotion-ad/node_modules' \ + --exclude='.env' --exclude='.env.local' --exclude='docker-compose.yml' \ + --delete \ + ./ unraid:/mnt/user/appdata/marketing/ + +# Rebuild and restart +ssh unraid "cd /mnt/user/appdata/marketing && docker compose down && docker compose build app && docker compose up -d" +``` + +### Claude Auth in Docker +Claude Max uses OAuth, tokens stored in macOS Keychain. For headless Docker: +1. Run `claude setup-token` locally, open the magic link in browser +2. Extract the access token: `security find-generic-password -s "Claude Code-credentials" -a "$(whoami)" -w` +3. The JSON has `claudeAiOauth.accessToken` — use just that value +4. Set `CLAUDE_CODE_OAUTH_TOKEN=` in the Unraid `.env` + +### Volume Permissions +Host directories must be owned by UID 1000 (node user in container): +```bash +ssh unraid "chown -R 1000:1000 /mnt/user/downloads/marketing/{db,outputs,knowledge}" +``` + +### Dockerfile Notes +- Base image: `node:20-slim` (NOT alpine — Playwright needs apt/Debian) +- Startup script (`start.sh`) runs `prisma db push` and `prisma db seed` before `node server.js` +- Seed is idempotent (uses upsert), safe to run on every restart + +## Key Files +- `lib/claude.ts` - Claude CLI spawn logic, pipeline orchestration, chat sessions +- `lib/auth.ts` - NextAuth config (trustHost: true for reverse proxy) +- `lib/scanner.ts` - scans pipeline output directories for assets +- `prisma/schema.prisma` - 8 models: User, App, Campaign, AgentRun, Asset, ClaudeSession, TrendReport, Setting +- `prisma/seed.ts` - creates admin user + honeyDue app +- `pipeline/CLAUDE.md` - agent system docs (read this for pipeline details) +- `pipeline/skills/` - 8 agent skill definitions +- `pipeline/knowledge/` - brand assets & guidelines + +## SSH +Unraid is accessible via `ssh unraid` (configured in ~/.ssh/config on dev machine). diff --git a/Dockerfile b/Dockerfile index 869944f..4d5ffe6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-alpine AS base +FROM node:20-slim AS base # Install Claude Code CLI RUN npm install -g @anthropic-ai/claude-code @@ -21,15 +21,30 @@ RUN npm run build FROM base AS runner WORKDIR /app ENV NODE_ENV=production +ENV HOME=/home/node + +# Create Claude credentials directory with correct ownership +RUN mkdir -p /home/node/.claude && chown -R node:node /home/node COPY --from=builder /app/.next/standalone ./ COPY --from=builder /app/.next/static ./.next/static COPY --from=builder /app/public ./public COPY --from=builder /app/prisma ./prisma COPY --from=builder /app/lib/generated ./lib/generated +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/package.json ./package.json +COPY --from=builder /app/prisma.config.ts ./prisma.config.ts # Copy the marketing pipeline COPY pipeline/ ./pipeline/ +# Startup script: ensure DB exists, then start server +RUN printf '#!/bin/sh\nnpx prisma db push 2>&1 || true\nnpx prisma db seed 2>&1 || true\nnode server.js\n' > /app/start.sh && chmod +x /app/start.sh + +# Ensure node user owns the app directory +RUN chown -R node:node /app + +USER node + EXPOSE 3000 -CMD ["node", "server.js"] +CMD ["/app/start.sh"] diff --git a/app/(dashboard)/apps/page.tsx b/app/(dashboard)/apps/page.tsx index 6aa58b8..dce9b4e 100644 --- a/app/(dashboard)/apps/page.tsx +++ b/app/(dashboard)/apps/page.tsx @@ -64,7 +64,7 @@ export default function AppsPage() {
{apps.map((app) => ( - +
{app.description || "No description"}

-
-
-
- {app.primaryColor} -
-
-
- {app.accentColor} -
- +
+
+
+ {app._count.campaigns} campaign{app._count.campaigns !== 1 ? "s" : ""}
diff --git a/app/(dashboard)/campaigns/page.tsx b/app/(dashboard)/campaigns/page.tsx index 53b7990..bddf2a3 100644 --- a/app/(dashboard)/campaigns/page.tsx +++ b/app/(dashboard)/campaigns/page.tsx @@ -11,7 +11,13 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import { Plus } from "lucide-react"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { Plus, Info } from "lucide-react"; interface Campaign { id: string; @@ -30,6 +36,25 @@ const statusColors: Record = { + draft: "Campaign is configured but hasn't been launched yet. Click to edit and launch.", + running: "The AI pipeline is actively generating assets for this campaign.", + review: "All agents finished. Assets are ready for your review before publishing.", + approved: "Assets have been approved and are ready to publish.", + published: "Campaign content has been pushed to social platforms.", +}; + +function Tip({ children, content }: { children: React.ReactNode; content: string }) { + return ( + + + {children} + + {content} + + ); +} + export default function CampaignsPage() { const [campaigns, setCampaigns] = useState([]); @@ -41,49 +66,72 @@ export default function CampaignsPage() { }, []); return ( -
-
-

Campaigns

- - - New Campaign - -
- - {campaigns.length === 0 ? ( - - -

- No campaigns yet. Create your first one to get started. -

-
-
- ) : ( -
- {campaigns.map((campaign) => { - const platforms = JSON.parse(campaign.platforms) as string[]; - return ( - - - -
- {campaign.name} - - {campaign.status} - -
- - {platforms.join(", ")} ·{" "} - {campaign._count.assets} assets ·{" "} - {new Date(campaign.createdAt).toLocaleDateString()} - -
-
- - ); - })} + +
+
+
+

Campaigns

+ + + +
+ + + New Campaign +
- )} -
+ + {campaigns.length === 0 ? ( + + +

+ No campaigns yet. Create your first one to get started. +

+
+
+ ) : ( +
+ {campaigns.map((campaign) => { + const platforms = JSON.parse(campaign.platforms) as string[]; + return ( + + + +
+ {campaign.name} + + + {campaign.status} + + +
+ + + + {platforms.join(", ")} + + + · + + + {campaign._count.assets} assets + + + · + + + {new Date(campaign.createdAt).toLocaleDateString()} + + + +
+
+ + ); + })} +
+ )} +
+ ); } diff --git a/app/api/apps/[slug]/route.ts b/app/api/apps/[slug]/route.ts index 6d25b1a..360ba15 100644 --- a/app/api/apps/[slug]/route.ts +++ b/app/api/apps/[slug]/route.ts @@ -43,6 +43,7 @@ export async function PATCH( brandIdentity: body.brandIdentity ?? undefined, productInfo: body.productInfo ?? undefined, platformGuidelines: body.platformGuidelines ?? undefined, + stylePreferences: body.stylePreferences ?? undefined, }, }); diff --git a/app/api/assets/[id]/preference/route.ts b/app/api/assets/[id]/preference/route.ts new file mode 100644 index 0000000..7ddf79c --- /dev/null +++ b/app/api/assets/[id]/preference/route.ts @@ -0,0 +1,156 @@ +import { auth } from "@/lib/auth"; +import { prisma } from "@/lib/prisma"; + +interface LikedEntry { + assetId: string; + filePath: string; + fileName: string; + dimensions: string | null; + platform: string | null; + style: string | null; // "with-people" | "without-people" | null + timestamp: string; +} + +interface DislikedEntry { + assetId: string; + reason: string; + fileName: string; + dimensions: string | null; + platform: string | null; + style: string | null; + timestamp: string; +} + +interface StylePreferences { + liked: LikedEntry[]; + disliked: DislikedEntry[]; +} + +function parsePreferences(raw: string | null): StylePreferences { + if (!raw) return { liked: [], disliked: [] }; + try { + const parsed = JSON.parse(raw); + return { + liked: Array.isArray(parsed.liked) ? parsed.liked : [], + disliked: Array.isArray(parsed.disliked) ? parsed.disliked : [], + }; + } catch { + return { liked: [], disliked: [] }; + } +} + +export async function GET( + _request: Request, + { params }: { params: Promise<{ id: string }> } +) { + const session = await auth(); + if (!session) return new Response("Unauthorized", { status: 401 }); + + const { id } = await params; + + const asset = await prisma.asset.findUnique({ + where: { id }, + include: { campaign: { include: { app: true } } }, + }); + + if (!asset) return Response.json({ error: "Not found" }, { status: 404 }); + + const app = asset.campaign?.app; + if (!app) return Response.json({ vote: null }); + + const prefs = parsePreferences(app.stylePreferences); + const isLiked = prefs.liked.some((e) => e.assetId === id); + const isDisliked = prefs.disliked.some((e) => e.assetId === id); + + return Response.json({ vote: isLiked ? "up" : isDisliked ? "down" : null }); +} + +export async function POST( + request: Request, + { params }: { params: Promise<{ id: string }> } +) { + const session = await auth(); + if (!session) return new Response("Unauthorized", { status: 401 }); + + const { id } = await params; + const body = await request.json(); + const { vote, reason } = body as { vote: "up" | "down"; reason?: string }; + + if (vote !== "up" && vote !== "down") { + return Response.json({ error: "vote must be 'up' or 'down'" }, { status: 400 }); + } + + const asset = await prisma.asset.findUnique({ + where: { id }, + include: { campaign: { include: { app: true } } }, + }); + + if (!asset) return Response.json({ error: "Not found" }, { status: 404 }); + + const app = asset.campaign?.app; + if (!app) { + return Response.json({ error: "Asset has no associated app" }, { status: 400 }); + } + + // Extract style from asset metadata (set by ad_manifest.json during generation) + let assetStyle: string | null = null; + if (asset.metadata) { + try { + const meta = JSON.parse(asset.metadata); + if (meta.style === "with-people" || meta.style === "without-people") { + assetStyle = meta.style; + } + } catch { /* metadata isn't JSON or has no style */ } + } + + const prefs = parsePreferences(app.stylePreferences); + + // Remove from both lists first (clean slate for this asset) + prefs.liked = prefs.liked.filter((e) => e.assetId !== id); + prefs.disliked = prefs.disliked.filter((e) => e.assetId !== id); + + // Check if this was already the active vote (toggle off) + const wasLiked = parsePreferences(app.stylePreferences).liked.some((e) => e.assetId === id); + const wasDisliked = parsePreferences(app.stylePreferences).disliked.some((e) => e.assetId === id); + const isToggleOff = (vote === "up" && wasLiked) || (vote === "down" && wasDisliked); + + if (!isToggleOff) { + if (vote === "up") { + prefs.liked.push({ + assetId: id, + filePath: asset.filePath, + fileName: asset.fileName, + dimensions: asset.dimensions || null, + platform: asset.platform || null, + style: assetStyle, + timestamp: new Date().toISOString(), + }); + // Keep max ~10 liked + if (prefs.liked.length > 10) { + prefs.liked = prefs.liked.slice(-10); + } + } else { + prefs.disliked.push({ + assetId: id, + reason: reason || "Not preferred", + fileName: asset.fileName, + dimensions: asset.dimensions || null, + platform: asset.platform || null, + style: assetStyle, + timestamp: new Date().toISOString(), + }); + // Keep max ~20 disliked + if (prefs.disliked.length > 20) { + prefs.disliked = prefs.disliked.slice(-20); + } + } + } + + await prisma.app.update({ + where: { id: app.id }, + data: { stylePreferences: JSON.stringify(prefs) }, + }); + + const newVote = isToggleOff ? null : vote; + return Response.json({ vote: newVote }); +} diff --git a/app/api/assets/[id]/repurpose/route.ts b/app/api/assets/[id]/repurpose/route.ts index ab238b0..8bfd6ca 100644 --- a/app/api/assets/[id]/repurpose/route.ts +++ b/app/api/assets/[id]/repurpose/route.ts @@ -1,6 +1,10 @@ import { auth } from "@/lib/auth"; import { prisma } from "@/lib/prisma"; import { repurposeImage, retoneCaption, getAvailableFormats } from "@/lib/repurpose"; +import { existsSync } from "fs"; +import path from "path"; + +const PIPELINE_ROOT = process.env.PIPELINE_ROOT || path.join(process.cwd(), "pipeline"); export async function GET( _request: Request, @@ -36,44 +40,57 @@ export async function POST( } const outputDir = `outputs/repurposed_${id.slice(0, 8)}`; - const resized = await repurposeImage(asset.filePath, targetFormats, outputDir); - const results = []; - for (const file of resized) { - const originalMeta = asset.metadata ? JSON.parse(asset.metadata) : {}; - let newMeta = { ...originalMeta }; + // Launch async — Gemini generation takes time + (async () => { + try { + const expectedFiles = await repurposeImage( + asset.filePath, + asset.dimensions, + targetFormats, + outputDir + ); - // Re-tone caption if platform changed and caption exists - if (originalMeta.caption && asset.platform && file.platform !== asset.platform) { - try { - const newCaption = await retoneCaption( - originalMeta.caption, - asset.platform, - file.platform - ); - newMeta = { ...newMeta, caption: newCaption, originalCaption: originalMeta.caption }; - } catch { - // Keep original caption if re-toning fails + // Create DB records for files that were actually generated + for (const file of expectedFiles) { + const fullPath = path.join(PIPELINE_ROOT, file.filePath); + if (!existsSync(fullPath)) continue; + + const originalMeta = asset.metadata ? JSON.parse(asset.metadata) : {}; + let newMeta = { ...originalMeta }; + + if (originalMeta.caption && asset.platform && file.platform !== asset.platform) { + try { + const newCaption = await retoneCaption( + originalMeta.caption, + asset.platform, + file.platform + ); + newMeta = { ...newMeta, caption: newCaption, originalCaption: originalMeta.caption }; + } catch { + // Keep original caption + } + } + + await prisma.asset.create({ + data: { + campaignId: asset.campaignId, + type: "image", + platform: file.platform, + format: "png", + filePath: file.filePath, + fileName: file.fileName, + dimensions: file.dimensions, + metadata: JSON.stringify(newMeta), + status: "draft", + parentAssetId: asset.id, + }, + }); } + } catch (err) { + console.error(`Repurpose failed for asset ${id}:`, err); } + })(); - const newAsset = await prisma.asset.create({ - data: { - campaignId: asset.campaignId, - type: "image", - platform: file.platform, - format: "png", - filePath: file.filePath, - fileName: file.fileName, - dimensions: file.dimensions, - metadata: JSON.stringify(newMeta), - status: "draft", - parentAssetId: asset.id, - }, - }); - - results.push(newAsset); - } - - return Response.json({ created: results.length, assets: results }); + return Response.json({ status: "repurposing", formats: targetFormats }); } diff --git a/app/api/assets/[id]/variations/route.ts b/app/api/assets/[id]/variations/route.ts index 716982e..8b12971 100644 --- a/app/api/assets/[id]/variations/route.ts +++ b/app/api/assets/[id]/variations/route.ts @@ -55,6 +55,7 @@ export async function POST( brandIdentity: app.brandIdentity, productInfo: app.productInfo, platformGuidelines: app.platformGuidelines, + stylePreferences: app.stylePreferences, }; } diff --git a/app/api/assets/route.ts b/app/api/assets/route.ts index d540190..533e4d0 100644 --- a/app/api/assets/route.ts +++ b/app/api/assets/route.ts @@ -17,7 +17,11 @@ export async function GET(request: Request) { const where: Record = {}; if (campaignId) where.campaignId = campaignId; - if (type && type !== "all") where.type = type; + if (type === "media") { + where.type = { in: ["image", "video"] }; + } else if (type && type !== "all") { + where.type = type; + } if (platform && platform !== "all") where.platform = platform; if (status && status !== "all") where.status = status; if (search) { diff --git a/app/api/campaigns/[id]/launch/route.ts b/app/api/campaigns/[id]/launch/route.ts index 6eebe2a..e531163 100644 --- a/app/api/campaigns/[id]/launch/route.ts +++ b/app/api/campaigns/[id]/launch/route.ts @@ -42,6 +42,7 @@ export async function POST( brandIdentity: app.brandIdentity, productInfo: app.productInfo, platformGuidelines: app.platformGuidelines, + stylePreferences: app.stylePreferences, }; } diff --git a/components/asset-card.tsx b/components/asset-card.tsx index 261f5f6..e90081a 100644 --- a/components/asset-card.tsx +++ b/components/asset-card.tsx @@ -1,11 +1,12 @@ "use client"; -import { useState } from "react"; +import { useState, useEffect } from "react"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; -import { Play, Copy, Sparkles, Send } from "lucide-react"; +import { Play, Copy, Sparkles, Send, ThumbsUp, ThumbsDown } from "lucide-react"; import { RepurposeModal } from "./repurpose-modal"; import { VariationModal } from "./variation-modal"; +import { ThumbsDownModal } from "./thumbs-down-modal"; interface Asset { id: string; @@ -27,6 +28,7 @@ interface AssetCardProps { selected?: boolean; onSelect?: (id: string) => void; onPushToPostiz?: (assetIds: string[]) => void; + onRefresh?: () => void; } export function AssetCard({ @@ -34,11 +36,21 @@ export function AssetCard({ selected, onSelect, onPushToPostiz, + onRefresh, }: AssetCardProps) { const [repurposeOpen, setRepurposeOpen] = useState(false); const [variationOpen, setVariationOpen] = useState(false); + const [thumbsDownOpen, setThumbsDownOpen] = useState(false); + const [vote, setVote] = useState<"up" | "down" | null>(null); - const metadata = asset.metadata ? JSON.parse(asset.metadata) : {}; + let metadata: Record = {}; + if (asset.metadata) { + try { + metadata = JSON.parse(asset.metadata); + } catch { + // metadata may be truncated — ignore parse errors + } + } const isImage = asset.type === "image" || asset.format === "png" || asset.format === "jpg"; const isVideo = asset.type === "video" || asset.format === "mp4"; const fileSrc = `/api/files/${asset.filePath}`; @@ -54,6 +66,34 @@ export function AssetCard({ ? "Playwright" : null; + const isVisual = isImage || isVideo; + + useEffect(() => { + if (!isVisual) return; + fetch(`/api/assets/${asset.id}/preference`) + .then((r) => r.json()) + .then((data) => setVote(data.vote ?? null)) + .catch(() => {}); + }, [asset.id, isVisual]); + + async function handleThumbsUp() { + const newVote = vote === "up" ? null : "up"; + setVote(newVote); // Optimistic + try { + const res = await fetch(`/api/assets/${asset.id}/preference`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ vote: "up" }), + }); + if (res.ok) { + const data = await res.json(); + setVote(data.vote); + } + } catch (err) { + console.error("Preference API failed:", err); + } + } + const sourceColors: Record = { Gemini: "text-purple-600 border-purple-200 bg-purple-50", "Canvas Design": "text-amber-600 border-amber-200 bg-amber-50", @@ -146,7 +186,7 @@ export function AssetCard({ )}
- {metadata.caption && ( + {typeof metadata.caption === "string" && (

{metadata.caption}

@@ -172,18 +212,61 @@ export function AssetCard({ )}
+ {/* Style Preference Buttons */} + {isVisual && ( +
+ + +
+ )} + {/* Actions */} {(isImage || isVideo) && ( -
+
{onPushToPostiz && ( )} {isImage && ( @@ -191,20 +274,20 @@ export function AssetCard({ )} @@ -216,7 +299,10 @@ export function AssetCard({ {repurposeOpen && ( setRepurposeOpen(false)} + onClose={() => { + setRepurposeOpen(false); + onRefresh?.(); + }} /> )} {variationOpen && ( @@ -226,6 +312,13 @@ export function AssetCard({ onClose={() => setVariationOpen(false)} /> )} + {thumbsDownOpen && ( + setThumbsDownOpen(false)} + onSubmitted={() => setVote("down")} + /> + )}
); } diff --git a/components/asset-gallery.tsx b/components/asset-gallery.tsx index 88750a6..f8df581 100644 --- a/components/asset-gallery.tsx +++ b/components/asset-gallery.tsx @@ -30,7 +30,7 @@ export function AssetGallery({ campaignId, onPushToPostiz }: AssetGalleryProps) const [selectedIds, setSelectedIds] = useState>(new Set()); const [filters, setFilters] = useState({ platform: "all", - type: "all", + type: "media", }); const [search, setSearch] = useState(""); const [sort, setSort] = useState("newest"); @@ -103,9 +103,10 @@ export function AssetGallery({ campaignId, onPushToPostiz }: AssetGalleryProps) setFilters((f) => ({ ...f, type: e.target.value })) } > + - - + + @@ -156,6 +157,7 @@ export function AssetGallery({ campaignId, onPushToPostiz }: AssetGalleryProps) selected={selectedIds.has(asset.id)} onSelect={toggleSelect} onPushToPostiz={onPushToPostiz} + onRefresh={fetchAssets} /> ))}
diff --git a/components/campaign-form.tsx b/components/campaign-form.tsx index 6bf67d1..e23d75d 100644 --- a/components/campaign-form.tsx +++ b/components/campaign-form.tsx @@ -13,7 +13,24 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import { ImagePlus, X, Loader2 } from "lucide-react"; +import { ImagePlus, X, Loader2, Info } from "lucide-react"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; + +function InfoTip({ text }: { text: string }) { + return ( + + + + + {text} + + ); +} export const PLATFORMS = ["instagram", "tiktok", "nextdoor"] as const; export const GOALS = [ @@ -221,6 +238,7 @@ export function CampaignForm({ initialData, mode = "create" }: CampaignFormProps } return ( + {mode === "edit" ? "Edit Campaign" : "New Campaign"} @@ -233,7 +251,7 @@ export function CampaignForm({ initialData, mode = "create" }: CampaignFormProps
- +
- +
{PLATFORMS.map((platform) => (