diff --git a/.gitignore b/.gitignore
index c34b3c1..ff4fffa 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,3 +45,4 @@ yarn-error.log*
next-env.d.ts
.env*.local
/services/client-pulse-bot/dist/
+/tmp/checkcat.mjs
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 674e733..c5ff6f3 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -133,6 +133,7 @@ model YoutubeIdea {
funnelStage String?
status String @default("pending")
rejectedReason String?
+ category String? @default("brands")
sourceUrl String?
sourceSummary String?
competitionScore String? // "gap" | "moderate" | "crowded"
@@ -172,6 +173,8 @@ model YoutubeScript {
// Source content (tweet/thread/news) rendered as visual cards
sourceUrl String?
sourceData Json?
+ // Content category (brands | ai-agent-tips | ...)
+ category String? @default("brands")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
diff --git a/scripts/render-worker.mjs b/scripts/render-worker.mjs
index 9b81bbc..5a261c1 100644
--- a/scripts/render-worker.mjs
+++ b/scripts/render-worker.mjs
@@ -29,6 +29,7 @@ const EDGE_TTS_BIN =
? "/home/code/.hermes/hermes-agent/venv/bin/edge-tts"
: "edge-tts");
const VOICE = process.env.TTS_VOICE || "en-US-ChristopherNeural";
+const VOICE_TIPS = process.env.TTS_VOICE_TIPS || "en-US-GuyNeural"; // tech/explainer
const CHROME = process.env.CHROME_BIN || "/usr/bin/google-chrome-stable";
const MAX_TOTAL_SEC = 60;
const FPS = 30;
@@ -111,12 +112,12 @@ function sh(cmd, args, opts = {}) {
return r;
}
-function tts(text, outBase) {
+function tts(text, outBase, voice = VOICE) {
const out = `${outBase}.mp3`;
const srt = `${outBase}.sentences.json`;
sh(
TTS_PYTHON,
- [TTS_WORDS_PY, text, VOICE, out, srt],
+ [TTS_WORDS_PY, text, voice, out, srt],
{ timeout: 60_000, env: { ...process.env } }
);
let sentences = [];
@@ -166,18 +167,29 @@ function extractStatData(text) {
/** Build segment text list from a YoutubeScript's structured fields. */
function buildSegments(script) {
+ const isTips = script.category === "ai-agent-tips";
const segs = [];
const push = (text, label, highlight) => {
const clean = (text || "").trim();
const chunk = clean.length > 180 ? clean.slice(0, 180) + "…" : clean;
if (chunk) segs.push({ text: chunk, label, highlight });
};
- push(script.hook, "HOOK", script.hook);
- push(script.onScreenText, "ON SCREEN", script.onScreenText);
- push(script.storySetup, "THE STORY", null);
- push(script.conflict, "THE CONFLICT", null);
- push(script.insight, "THE INSIGHT", null);
- push(script.cta, "CTA", null);
+ // For AI-agent tips: labels = MISTAKE / THE FIX instead of THE STORY / ...
+ if (isTips) {
+ push(script.hook, "HOOK", script.hook);
+ push(script.onScreenText, "ON SCREEN", script.onScreenText);
+ push(script.storySetup, "THE MISTAKE", null);
+ push(script.conflict, "WHY IT FAILS", null);
+ push(script.insight, "THE FIX", null);
+ push(script.cta, "CTA", null);
+ } else {
+ push(script.hook, "HOOK", script.hook);
+ push(script.onScreenText, "ON SCREEN", script.onScreenText);
+ push(script.storySetup, "THE STORY", null);
+ push(script.conflict, "THE CONFLICT", null);
+ push(script.insight, "THE INSIGHT", null);
+ push(script.cta, "CTA", null);
+ }
if (segs.length === 0) push(script.fullScript || script.title, script.title || "SHORTS", null);
return segs;
}
@@ -194,7 +206,7 @@ async function renderScript(script) {
const voiced = [];
for (let i = 0; i < baseSegs.length; i++) {
const s = baseSegs[i];
- const t = tts(s.text, path.join(work, `seg${i}`));
+ const t = tts(s.text, path.join(work, `seg${i}`), script.category === "ai-agent-tips" ? VOICE_TIPS : VOICE);
voiced.push({ ...s, ...t });
log(`[${id}] seg${i}: ${t.durationSec.toFixed(2)}s "${s.text.slice(0, 40)}"`);
}
diff --git a/src/app/api/youtube/generate/route.ts b/src/app/api/youtube/generate/route.ts
index b0ea822..e613628 100644
--- a/src/app/api/youtube/generate/route.ts
+++ b/src/app/api/youtube/generate/route.ts
@@ -45,8 +45,9 @@ function fetchArticle(url: string): string {
}
export async function POST(req: NextRequest) {
- const { topic, sourceUrl, sourceSummary } = await req.json();
+ const { topic, sourceUrl, sourceSummary, category } = await req.json();
if (!topic) return NextResponse.json({ error: "topic required" }, { status: 400 });
+ const isTips = category === "ai-agent-tips";
const apiKey = process.env.OPENAI_API_KEY || "";
const braveKey = process.env.BRAVE_API_KEY || "";
@@ -171,20 +172,25 @@ ${sourceContext || "No sources available — be EXTRA cautious with claims, hedg
- Include a "factClaims" array listing every specific factual claim in the script (dates, numbers, events, quotes)
- Each claim MUST be traceable to the source material above
-## THE USER'S ANGLE
+## THE USER'S ANGLE${isTips ? `
+- Builds and ships AI agents (coding agents, MCP, agent loops) — the tips are for OTHER builders
+- Voice: direct, first-principles, tells you the mistake you are probably making
+- Every tip must be concrete and actionable — no fluff
+- The viewer is technical: they write code and wire tools together
+- Ground each tip in a real failure/lesson: "I ran this in production and learned X"` : `
- Runs a marketing-focused business
- Content: founder branding, marketing, distribution > product, culture > features
-- Voice: conversational, no fancy words, lowercase energy, real numbers, honest/vulnerable
-${pastScriptsRef}
-${feedbackContext}
+- Voice: conversational, no fancy words, lowercase energy, real numbers, honest/vulnerable`}
+ ${pastScriptsRef}
+ ${feedbackContext}
OUTPUT FORMAT — valid JSON only:
{
"title": "Brand/Person — Core Lesson (short)",
- "hook": "Opening 2-4 lines. Scroll-stopping.",
- "storySetup": "The backstory context. 3-5 sentences.",
- "conflict": "The tension pivot or controversial statement. 3-5 sentences.",
- "insight": "The aha moment payoff lesson for founders. 3-5 sentences.",
+ "hook": "Opening 2-4 lines. Scroll-stopping.${isTips ? " Lead with the COMMON MISTAKE or counter-intuitive truth about AI agents." : ""}",
+ "storySetup": "The backstory context. 3-5 sentences.${isTips ? " The situation: why builders get this wrong." : ""}",
+ "conflict": "The tension pivot or controversial statement. 3-5 sentences.${isTips ? " The failure mode / what everyone does that backfires." : ""}",
+ "insight": "The aha moment payoff lesson for founders. 3-5 sentences.${isTips ? " THE concrete fix/pattern — specific, actionable, names the tool/technique." : ""}",
"cta": "One clear action. One line.",
"caption": "One punchy line without spoilers.",
"onScreenText": "What appears on screen during the hook",
diff --git a/src/app/api/youtube/ideas/generate/route.ts b/src/app/api/youtube/ideas/generate/route.ts
index 984c9e5..2dfbbef 100644
--- a/src/app/api/youtube/ideas/generate/route.ts
+++ b/src/app/api/youtube/ideas/generate/route.ts
@@ -33,7 +33,11 @@ function braveSearch(query: string, braveKey: string, count = 5): { title: strin
}
}
-export async function POST() {
+export async function POST(req?: Request) {
+ const body = req ? await req.json().catch(() => ({})) : {};
+ const category: string = body.category || "brands";
+ const isTips = category === "ai-agent-tips";
+
const apiKey = process.env.OPENAI_API_KEY || "";
const braveKey = process.env.BRAVE_API_KEY || "";
if (!apiKey) return NextResponse.json({ error: "No API key" }, { status: 500 });
@@ -65,10 +69,19 @@ export async function POST() {
try { coveredTopics = fs.readFileSync(SCRIPTS_FILE, "utf-8").match(/### .+/g)?.join(", ") || ""; } catch { /* ok - file may not exist on Vercel */ }
// -- STEP 1: Ask GPT to generate search queries --
- const queryGenResult = callOpenAI(apiKey, {
- model: "gpt-4o-mini",
- messages: [
- { role: "system", content: `You generate search queries to find fascinating REAL stories about brands, businesses, and cultural moments for a viral short-form video creator.
+ const querySystem = isTips
+ ? `You generate search queries to find practical, actionable AI AGENT tips and best practices for a viral short-form video creator.
+
+The creator makes videos for builders/developers who ship AI agents (coding agents, MCP servers, tool-using LLMs, agent loops).
+Topics to cover: agent architecture, tool design, context window management, evals/testing, prompt engineering for agents, MCP, sub-agents, cost control, reliability/failure modes, memory, permissions/safety, orchestration patterns.
+
+Generate 10 diverse search queries for REAL, concrete tips (engineering blogs, docs, HN discussions, practitioner posts). Prefer "best practices", "lessons learned", "common mistakes", "how to", "patterns".
+
+DO NOT search for ideas already generated: ${existingTitles}
+${rejectionContext}
+
+Output JSON: {"queries":["search query 1","search query 2",...]}`
+ : `You generate search queries to find fascinating REAL stories about brands, businesses, and cultural moments for a viral short-form video creator.
The creator covers a WIDE mix: mainstream brands (Red Bull, Nike, Barbie, Tesla, Apple, McDonald's, etc.), tech, pop culture, and business strategy. Most videos are about mainstream brands with a marketing/founder lesson.
@@ -81,8 +94,15 @@ DO NOT search for stories already covered: ${coveredTopics}
DO NOT search for ideas already generated: ${existingTitles}
${rejectionContext}
-Output JSON: {"queries":["search query 1","search query 2",...]}` },
- { role: "user", content: "10 diverse search queries for fascinating real stories. Mix mainstream brands, crypto, pop culture." }
+Output JSON: {"queries":["search query 1","search query 2",...]}`;
+
+ const queryGenResult = callOpenAI(apiKey, {
+ model: "gpt-4o-mini",
+ messages: [
+ { role: "system", content: querySystem },
+ { role: "user", content: isTips
+ ? "10 diverse search queries for real, actionable AI agent tips and lessons."
+ : "10 diverse search queries for fascinating real stories. Mix mainstream brands, crypto, pop culture." }
],
temperature: 0.95,
response_format: { type: "json_object" }
@@ -104,10 +124,23 @@ Output JSON: {"queries":["search query 1","search query 2",...]}` },
).join("\n\n");
// -- STEP 3: Feed real articles to GPT to create ideas --
- const ideaGenResult = callOpenAI(apiKey, {
- model: "gpt-4o-mini",
- messages: [
- { role: "system", content: `You create viral short-form video ideas for the user based on REAL articles and stories found via web search.
+ const ideaSystem = isTips
+ ? `You create viral short-form video IDEAS about practical AI AGENT tips for builders, based on REAL articles found via web search.
+
+CRITICAL RULES:
+- Each idea MUST be one concrete, actionable tip/lesson from the search results below
+- Each idea MUST include a sourceUrl from the search results
+- Each idea MUST include a sourceSummary (1-2 sentence summary of the real tip/lesson)
+- Hook must stop the scroll in 3 seconds — lead with a common MISTAKE or counter-intuitive truth
+- Examples of strong hooks: "Your AI agent fails because of one missing tool", "Stop giving your agent 40 tools", "Nobody tells you this about MCP servers"
+- The angle must name a SPECIFIC technique/tool/pattern (MCP, tool schemas, evals, context pruning, sub-agents...) — never vague "use AI better"
+- Mix funnel stages: 5 TOF (wide builder appeal), 3 MOF (specific workflows), 2 BOF (advanced architecture)
+- DO NOT repeat these existing ideas: ${existingTitles}
+- Each hook should have normal punctuation (commas, periods)
+${rejectionContext}
+
+Output JSON: {"ideas":[{"title":"short title","hook":"2-3 line spoken word scroll-stopper","angle":"specific technique/pattern","hookType":"observation|experience|expert|controversial","funnelStage":"TOF|MOF|BOF","sourceUrl":"url from search results","sourceSummary":"1-2 sentence summary of the real tip"}]}`
+ : `You create viral short-form video ideas for the user based on REAL articles and stories found via web search.
CRITICAL RULES:
- Each idea MUST be based on a REAL story from the search results below
@@ -124,8 +157,15 @@ CRITICAL RULES:
- Always name the SPECIFIC brand/person — never be vague
${rejectionContext}
-Output JSON: {"ideas":[{"title":"short title","hook":"2-3 line spoken word scroll-stopper","angle":"founder/crypto lesson","hookType":"observation|experience|expert|controversial","funnelStage":"TOF|MOF|BOF","sourceUrl":"url from search results","sourceSummary":"1-2 sentence summary of the real story"}]}` },
- { role: "user", content: `Based on these REAL articles found via web search, create 10 video ideas. Each idea MUST reference a real story and include the source URL.\n\nSEARCH RESULTS:\n${searchContext}\n\nMake the hooks FIRE.` }
+Output JSON: {"ideas":[{"title":"short title","hook":"2-3 line spoken word scroll-stopper","angle":"founder/crypto lesson","hookType":"observation|experience|expert|controversial","funnelStage":"TOF|MOF|BOF","sourceUrl":"url from search results","sourceSummary":"1-2 sentence summary of the real story"}]}`;
+
+ const ideaGenResult = callOpenAI(apiKey, {
+ model: "gpt-4o-mini",
+ messages: [
+ { role: "system", content: ideaSystem },
+ { role: "user", content: isTips
+ ? `Based on these REAL articles found via web search, create 10 AI AGENT TIP video ideas. Each idea MUST be one concrete tip and include the source URL.\n\nSEARCH RESULTS:\n${searchContext}\n\nMake the hooks FIRE.`
+ : `Based on these REAL articles found via web search, create 10 video ideas. Each idea MUST reference a real story and include the source URL.\n\nSEARCH RESULTS:\n${searchContext}\n\nMake the hooks FIRE.` }
],
temperature: 0.85,
response_format: { type: "json_object" }
@@ -149,6 +189,7 @@ Output JSON: {"ideas":[{"title":"short title","hook":"2-3 line spoken word scrol
angle: idea.angle || null,
hookType: idea.hookType || null,
funnelStage: idea.funnelStage || null,
+ category,
status: "pending",
},
});
diff --git a/src/app/youtube/page.tsx b/src/app/youtube/page.tsx
index cd42f09..a0e77eb 100644
--- a/src/app/youtube/page.tsx
+++ b/src/app/youtube/page.tsx
@@ -22,15 +22,17 @@ const LongformTab = dynamic(() => import("@/app/longform/page"), { ssr: false, l
const OutlierFeed = dynamic(() => import("@/components/OutlierFeed"), { ssr: false, loading: () => });
interface Idea {
+ id: string;
title: string;
- hook: string;
- angle: string;
+ hook?: string;
+ angle?: string;
hookType?: string;
funnelStage?: string;
- rejectedReason?: string;
- status?: "pending" | "rejected" | "approved";
+ category?: string;
sourceUrl?: string;
sourceSummary?: string;
+ rejectedReason?: string;
+ status?: "pending" | "rejected" | "approved";
}
interface Script {
@@ -82,6 +84,7 @@ export default function YouTubePage() {
const [batchGenerating, setBatchGenerating] = useState(false);
const [ideaTab, setIdeaTab] = useState<"pending" | "rejected">("pending");
const [scriptTab, setScriptTab] = useState<"draft" | "approved" | "rejected">("draft");
+ const [ideaCategory, setIdeaCategory] = useState<"brands" | "ai-agent-tips">("brands");
const [rejectModal, setRejectModal] = useState<{ type: "idea" | "script"; id: string; title: string } | null>(null);
const rejectRef = React.useRef(null);
@@ -139,7 +142,7 @@ export default function YouTubePage() {
const res = await fetch("/api/youtube/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
- body: JSON.stringify({ topic: `${idea.title}. Hook angle: ${idea.hook}. Lesson: ${idea.angle}`, sourceUrl: idea.sourceUrl, sourceSummary: idea.sourceSummary }),
+ body: JSON.stringify({ topic: `${idea.title}. Hook angle: ${idea.hook}. Lesson: ${idea.angle}`, sourceUrl: idea.sourceUrl, sourceSummary: idea.sourceSummary, category: idea.category || ideaCategory }),
});
if (res.ok) {
const data = await res.json();
@@ -629,11 +632,24 @@ export default function YouTubePage() {
}}
>{batchGenerating ? `✨ Generating ${selectedIdeas.size}...` : `Generate ${selectedIdeas.size} Script${selectedIdeas.size > 1 ? "s" : ""}`}
)}
+