AI Service
Client-side TypeScript service for AI content generation. All functions call server-side API endpoints.
Location: src/services/ai.ts
Overview
The AI service provides functions for:
- PRF Generation - Podcast Repurposing Framework from transcripts
- Viral Hooks - Social media hooks from PRF
- Episode Metadata - Titles, descriptions, takeaways, show notes
- Social Posts - LinkedIn, Instagram, Twitter content
- Infographic Specs - Design specifications for image generation
- Visual Suggestions - Automated visual content recommendations
- Fact Checking - Verify generated content against transcript
All generation uses Claude Sonnet 4 with RAG-enhanced prompts from the Pinecone knowledge base.
generatePRF()
Generate PRF document from transcript.
function generatePRF(transcript: string): Promise<string>
Parameters
Returns
string // PRF document in Markdown format
Example
import { generatePRF } from '@/services/ai'
const transcript = `385-Chris Pacifico\nGuest: Chris Pacifico\n...`
try {
const prf = await generatePRF(transcript)
console.log('PRF generated:', prf.length, 'characters')
// Save to episode
await updateEpisode(episodeId, { prf })
} catch (error) {
console.error('PRF generation failed:', error.message)
}
Automatic RAG: Fetches brand guidelines and writing style from Pinecone. Uses the PRF agent prompt from settings.
generateHooks()
Generate viral social media hooks from PRF.
function generateHooks(
transcript: string,
prf: string,
episodeNumber: string,
guestName: string
): Promise<string>
Parameters
Full episode transcript (for fact verification)
Episode number (e.g., “385”)
Returns
string // HTML content with TipTap-compatible structure
Example
import { generateHooks } from '@/services/ai'
const hooks = await generateHooks(
transcript,
prf,
'385',
'Chris Pacifico'
)
// hooks is HTML:
// "<h2>Hook 1: The Hidden Cost</h2>\n<p>Most IT leaders don't realize...</p>"
await updateEpisode(episodeId, { viralHooks: hooks })
Fact Verification Required: Always pass the full transcript to enable fact-checking. The AI cross-references claims against the source.
Generate comprehensive episode metadata using agentic workflow with SSE progress updates.
function generateEpisodeMetadata(
episodeId: string,
episodeNumber: string,
guestName: string,
transcript: string,
prf?: string,
onProgress?: (step: string, detail: string, progress: number) => void
): Promise<EpisodeMetadataResult>
Parameters
PRF document (optional but recommended)
Progress callback: (step, detail, progress) => void
Returns
interface EpisodeMetadataResult {
title: string // "EP 385: The Hidden Cost of Vendor Lock-In"
shortDescription: string // 1-2 sentence summary
longDescription: string // Full "On this episode" content
keyTakeaways: [string, string, string] // Exactly 3 takeaways
showNotes: ShowNote[] // Timestamped segments
}
interface ShowNote {
timestamp: string // "00:15:30"
description: string
}
Example
import { generateEpisodeMetadata } from '@/services/ai'
const metadata = await generateEpisodeMetadata(
episodeId,
'385',
'Chris Pacifico',
transcript,
prf,
(step, detail, progress) => {
console.log(`[${Math.round(progress * 100)}%] ${step}: ${detail}`)
}
)
// Save to Sanity
await updateEpisode(episodeId, {
sanityPageMetadata: metadata
})
SSE Streaming: Progress updates stream via Server-Sent Events. Useful for showing real-time status in UI.
generateTitleVariations()
Generate 3 viral title variations (contrarian, confession, specific angles).
function generateTitleVariations(
episodeId: string,
episodeNumber: string,
guestName: string,
transcript: string,
prf?: string,
existingTitles?: string[]
): Promise<TitleVariation[]>
Parameters
Previous titles to avoid repetition
Returns
interface TitleVariation {
title: string
angle: 'contrarian' | 'confession' | 'specific'
wordCount: number
}
Example
import { generateTitleVariations } from '@/services/ai'
const variations = await generateTitleVariations(
episodeId,
'385',
'Chris Pacifico',
transcript,
prf,
['The IT Leadership Crisis'] // Avoid similar titles
)
// variations:
// [
// { title: "Why Most CIOs Fail...", angle: "contrarian", wordCount: 8 },
// { title: "I Wasted 3 Years...", angle: "confession", wordCount: 6 },
// { title: "387 IT Leaders Reveal...", angle: "specific", wordCount: 7 }
// ]
generateLinkedInPosts()
Generate LinkedIn posts with verified facts bank.
function generateLinkedInPosts(
transcript: string,
prf: string,
hooks: string,
episodeNumber: string,
guestName: string
): Promise<LinkedInPostsResult>
Returns
interface LinkedInPostsResult {
verifiedFactsBank: {
directQuotes: string[] // Verbatim quotes from transcript
specificNumbers: string[] // Stats mentioned in episode
events: string[] // Specific events or examples
frameworks: string[] // Named frameworks or methodologies
insights: string[] // Key insights
}
releaseDay: string // Release day post copy
followUp: string // Follow-up post copy
}
Example
import { generateLinkedInPosts } from '@/services/ai'
const posts = await generateLinkedInPosts(
transcript,
prf,
hooks,
'385',
'Chris Pacifico'
)
await updateEpisode(episodeId, {
socialPosts: {
linkedin: [posts.releaseDay, posts.followUp]
}
})
Verified Facts Bank: The AI extracts quotes, numbers, and frameworks directly from the transcript to prevent hallucinations.
generateInfographicSpec()
Generate infographic design specification.
function generateInfographicSpec(
selectedText: string,
transcript: string,
prf: string,
episodeNumber: string,
guestName: string,
history: string[],
designType?: 'infographic' | 'quote' | 'thumbnail'
): Promise<InfographicSpec>
Parameters
Text excerpt to visualize
Previously used layout types (for variety)
designType
string
default:"infographic"
Design type: infographic, quote, or thumbnail
Returns
interface InfographicSpec {
layout: string // "Pyramid (Bottom-Up)"
template: string // "Strategic Framework"
title: string // "The Three Pillars of IT Leadership"
colorSystem: string // "Corporate Professional"
iconStyle: 'isometric' | 'flat2d'
aspectRatio: string // "16:9"
contentBreakdown?: {
mainMessage: string
sections: string[]
dataPoints: string[]
}
prompt: string // Complete Kie.ai prompt
}
Example
import { generateInfographicSpec } from '@/services/ai'
const spec = await generateInfographicSpec(
'The three pillars of IT leadership: technical expertise, business acumen, and people skills.',
transcript,
prf,
'385',
'Chris Pacifico',
['Doom Loop', 'Timeline'], // Avoid these layouts
'infographic'
)
// Use spec to generate image
import { createTask } from '@/services/kieai'
const { taskId } = await createTask({
prompt: spec.prompt,
aspectRatio: spec.aspectRatio,
resolution: '2K'
})
generateVisualSuggestionsV2()
Generate 10 visual suggestions (4 data viz + 4 cinematic + 2 quote cards) in parallel.
function generateVisualSuggestionsV2(
episodeId: string,
episodeNumber: string,
guestName: string,
transcript: string,
prf: string,
hooks: string,
history?: string[],
onProgress?: (step: string, detail: string, progress: number) => void,
customPrompts?: {
dataviz?: string
cinematic?: string
quoteCards?: string
}
): Promise<VisualSuggestionsResponse>
Returns
interface VisualSuggestionsResponse {
suggestions: VisualSuggestionResult[]
counts: {
dataviz: number // 4
cinematic: number // 4
quoteCard: number // 2
}
errors: {
dataviz: string | null
cinematic: string | null
quoteCard: string | null
}
}
interface VisualSuggestionResult {
visualType: 'dataviz' | 'cinematic' | 'quoteCard'
type: 'infographic' | 'quoteCard'
sourceText: string
sourceSection: string // "Core Message", "Key Insight #2", etc.
spec: InfographicSpec
}
Example
import { generateVisualSuggestionsV2 } from '@/services/ai'
const { suggestions, counts } = await generateVisualSuggestionsV2(
episodeId,
'385',
'Chris Pacifico',
transcript,
prf,
hooks,
[], // No history
(step, detail, progress) => {
console.log(`${Math.round(progress * 100)}%: ${step}`)
}
)
console.log(`Generated ${counts.dataviz} data viz + ${counts.cinematic} cinematic + ${counts.quoteCard} quote cards`)
// Save to episode
await updateEpisode(episodeId, {
visualSuggestions: suggestions
})
Parallel Generation: Uses agentic orchestrator to generate all 10 suggestions in parallel (~60-90 seconds total).
factCheckContent()
Verify generated content against transcript to prevent hallucinations.
function factCheckContent(
transcript: string,
prf: string,
guestName: string,
items: FactCheckItem[],
coHostName?: string,
onProgress?: (step: string, detail: string, progress: number) => void
): Promise<FactCheckResponse>
Parameters
Items to verifyinterface FactCheckItem {
type: 'statistic' | 'quote' | 'claim' | 'list' | 'attribution'
content: string
source?: string // e.g., guest name
}
Returns
interface FactCheckResponse {
overallScore: number // 0-100
passedValidation: boolean // true if score >= 80
results: ValidationResult[]
summary: string
criticalIssues: string[] // Blocking errors
}
interface ValidationResult {
item: FactCheckItem
status: 'verified' | 'unverified' | 'misattributed' | 'fabricated'
confidence: number // 0-1
issue?: string // Error description
suggestion?: string // How to fix
transcriptEvidence?: string // Supporting quote from transcript
}
Example
import { factCheckContent, extractFactCheckItems } from '@/services/ai'
// Extract items from infographic spec
const items = extractFactCheckItems(spec, guestName)
// Verify against transcript
const result = await factCheckContent(
transcript,
prf,
'Chris Pacifico',
items,
'Doug'
)
if (!result.passedValidation) {
console.error('Fact-check failed:', result.summary)
console.error('Issues:', result.criticalIssues)
// Show validation errors in UI
for (const r of result.results) {
if (r.status === 'fabricated') {
console.error(`❌ ${r.item.content}`)
console.error(` Issue: ${r.issue}`)
console.error(` Fix: ${r.suggestion}`)
}
}
}
Critical for Quality: Always fact-check generated content before saving suggestions. Prevents publishing hallucinated data.
Extract verifiable facts from an infographic spec.
function extractFactCheckItems(
spec: Record<string, unknown>,
guestName: string
): FactCheckItem[]
Example
import { extractFactCheckItems } from '@/services/ai'
const spec = {
title: "Mark Baker's Tech Debt Doom Loop", // NOT extracted (editorial title)
dataPoints: [
"70% of IT budgets go to maintenance", // EXTRACTED (statistic)
"Average lifespan: 3-5 years" // EXTRACTED (data)
],
pullQuote: {
text: "We were spending more time firefighting than innovating", // EXTRACTED (quote)
attribution: "Chris Pacifico"
},
sections: [
{
name: "The Cycle",
textContent: "1. Legacy systems age\n2. Technical debt grows\n3. Innovation stalls" // EXTRACTED (list)
}
]
}
const items = extractFactCheckItems(spec, 'Chris Pacifico')
// Returns 4 items (2 statistics, 1 quote, 1 list)
Titles Not Extracted: Editorial titles (e.g., “Mark Baker’s Doom Loop”) are not fact-checked because they’re creative descriptions, not claims made by the guest.
checkSpelling()
Check text for spelling and grammar errors.
function checkSpelling(text: string): Promise<SpellCheckResult>
Returns
interface SpellCheckResult {
correctedText: string
changes: SpellCheckChange[]
hasChanges: boolean
}
interface SpellCheckChange {
original: string
corrected: string
type: 'spelling' | 'grammar' | 'punctuation'
}
Example
import { checkSpelling } from '@/services/ai'
const result = await checkSpelling(
'Most IT leaders dont realize the impact of techincal debt.'
)
if (result.hasChanges) {
console.log('Corrected:', result.correctedText)
// "Most IT leaders don't realize the impact of technical debt."
console.log('Changes:', result.changes)
// [
// { original: "dont", corrected: "don't", type: "spelling" },
// { original: "techincal", corrected: "technical", type: "spelling" }
// ]
}
Settings Integration
All AI functions automatically use settings from settingsStore:
// Settings are fetched from store
const settings = useSettingsStore.getState().settings
// Used for:
// - Model selection (e.g., "claude-sonnet-4-20250514")
// - Agent prompts (PRF, hooks, infographic, etc.)
// - Preset fields (brand name, co-host name, etc.)
No need to pass these manually - they’re injected automatically.
Error Handling
All functions throw errors that should be caught:
try {
const prf = await generatePRF(transcript)
} catch (error) {
if (error.message.includes('API error: 401')) {
// Authentication expired
redirectToLogin()
} else if (error.message.includes('rate limit')) {
// Rate limit hit
showToast('Too many requests, please wait')
} else {
// Generation failed
showToast('Generation failed: ' + error.message)
}
}
Common error messages:
"Transcript is required"
"API error: 401" - Authentication issue
"API error: 429" - Rate limit
"Failed to generate [content]" - Generation error
"Anthropic API error: 500" - Upstream API issue