Skip to Content
📚 MyStoryFlow Docs — Your guide to preserving family stories

F007 - Manuscript Analysis Workflows

Objective

Implement comprehensive AI-powered manuscript analysis workflows that process uploaded manuscripts through multiple analysis dimensions, providing authors with actionable insights and improvement recommendations.

Quick Implementation

Using MyStoryFlow Components

  • Queue system from @mystoryflow/queue
  • Background job processing from @mystoryflow/jobs
  • Notification service from @mystoryflow/notifications
  • Progress tracking from @mystoryflow/analytics

New Requirements

  • Analysis orchestration service
  • Chapter detection and parsing
  • Multi-dimensional analysis engine
  • Result aggregation and scoring
  • Report generation system

MVP Implementation

1. Analysis Orchestrator

// packages/manuscript-analysis/src/services/analysis-orchestrator.ts import { AIService } from './ai-service' import { ChapterParser } from './chapter-parser' import { BatchProcessor } from './batch-processor' import { Logger } from '@mystoryflow/logger' import { QueueService } from '@mystoryflow/queue' import { NotificationService } from '@mystoryflow/notifications' export class AnalysisOrchestrator { private aiService: AIService private chapterParser: ChapterParser private batchProcessor: BatchProcessor private queue: QueueService private notifications: NotificationService private logger: Logger constructor() { this.aiService = new AIService() this.chapterParser = new ChapterParser() this.batchProcessor = new BatchProcessor() this.queue = new QueueService('manuscript-analysis') this.notifications = new NotificationService() this.logger = new Logger('AnalysisOrchestrator') } async analyzeManuscript(params: { manuscriptId: string userId: string organizationId: string content: string metadata: ManuscriptMetadata }): Promise<string> { // Create analysis job const jobId = await this.queue.createJob({ type: 'manuscript-analysis', payload: params, priority: this.getPriority(params.metadata.userTier) }) // Start async processing this.processAnalysis(jobId, params).catch(error => { this.logger.error('Analysis failed', { jobId, error }) this.handleAnalysisFailure(jobId, params.userId, error) }) return jobId } private async processAnalysis( jobId: string, params: AnalysisParams ): Promise<void> { try { // Update job status await this.queue.updateJob(jobId, { status: 'processing' }) // Parse manuscript structure const structure = await this.chapterParser.parseManuscript(params.content) await this.updateProgress(jobId, 10, 'Manuscript structure analyzed') // Perform comprehensive analysis const analyses = await this.performAnalyses( params.manuscriptId, structure, params.metadata ) await this.updateProgress(jobId, 80, 'All analyses completed') // Generate final report const report = await this.generateReport(analyses, structure, params.metadata) await this.updateProgress(jobId, 90, 'Report generated') // Store results await this.storeResults(params.manuscriptId, analyses, report) // Complete job await this.queue.completeJob(jobId, { reportId: report.id }) // Notify user await this.notifications.send({ userId: params.userId, type: 'analysis-complete', data: { manuscriptId: params.manuscriptId, reportId: report.id, summary: report.executiveSummary } }) } catch (error) { await this.queue.failJob(jobId, error.message) throw error } } private async performAnalyses( manuscriptId: string, structure: ManuscriptStructure, metadata: ManuscriptMetadata ): Promise<ComprehensiveAnalysis> { const analysisTypes = this.getAnalysisTypes(metadata.userTier) // Overall manuscript analysis const overallAnalysis = await this.aiService.analyzeManuscript( structure.fullText, 'overall', { genre: metadata.genre } ) await this.updateProgress(jobId, 30, 'Overall analysis complete') // Chapter-by-chapter analysis const chapterAnalyses = await this.batchProcessor.processManuscriptBatch( manuscriptId, structure.fullText, ['character', 'plot', 'pacing'] ) await this.updateProgress(jobId, 50, 'Chapter analyses complete') // Specialized analyses based on genre const specializedAnalyses = await this.performSpecializedAnalyses( structure, metadata.genre, analysisTypes ) await this.updateProgress(jobId, 70, 'Specialized analyses complete') return { overall: overallAnalysis, chapters: chapterAnalyses, specialized: specializedAnalyses, metadata: { totalWords: structure.wordCount, chapterCount: structure.chapters.length, avgChapterLength: Math.round(structure.wordCount / structure.chapters.length), genre: metadata.genre, analysisDate: new Date() } } } private async performSpecializedAnalyses( structure: ManuscriptStructure, genre: string, analysisTypes: string[] ): Promise<Record<string, any>> { const specialized = {} // Genre-specific analyses if (genre === 'romance') { specialized.emotionalArcs = await this.analyzeEmotionalArcs(structure) specialized.relationshipDynamics = await this.analyzeRelationships(structure) } else if (genre === 'mystery' || genre === 'thriller') { specialized.plotTwists = await this.analyzePlotTwists(structure) specialized.suspenseBuilding = await this.analyzeSuspense(structure) } else if (genre === 'fantasy' || genre === 'sci-fi') { specialized.worldBuilding = await this.analyzeWorldBuilding(structure) specialized.magicSystems = await this.analyzeMagicSystems(structure) } // Common specialized analyses if (analysisTypes.includes('dialogue')) { specialized.dialogueAnalysis = await this.analyzeDialogue(structure) } if (analysisTypes.includes('market')) { specialized.marketAnalysis = await this.analyzeMarketFit(structure, genre) } return specialized } private getAnalysisTypes(userTier: string): string[] { const tiers = { free: ['overall', 'basic-character', 'basic-plot'], starter: ['overall', 'character', 'plot', 'pacing', 'style'], professional: ['overall', 'character', 'plot', 'pacing', 'style', 'dialogue', 'market'], enterprise: ['all'] } return tiers[userTier] || tiers.free } }

2. Chapter Parser Service

// packages/manuscript-analysis/src/services/chapter-parser.ts export class ChapterParser { private patterns = { numbered: /^(Chapter|CHAPTER)\s+(\d+|[IVX]+)/m, named: /^(Chapter|CHAPTER)\s+(\d+|[IVX]+)[\s:]+(.+)/m, separator: /^\*{3,}|^-{3,}|^_{3,}/m, sceneBreak: /^\s*\*\s*\*\s*\*\s*$/m } async parseManuscript(content: string): Promise<ManuscriptStructure> { const lines = content.split('\n') const chapters: Chapter[] = [] let currentChapter: Chapter | null = null let currentContent: string[] = [] for (let i = 0; i < lines.length; i++) { const line = lines[i] const chapterMatch = this.detectChapter(line) if (chapterMatch) { // Save previous chapter if (currentChapter) { currentChapter.content = currentContent.join('\n') currentChapter.wordCount = this.countWords(currentChapter.content) chapters.push(currentChapter) } // Start new chapter currentChapter = { number: chapters.length + 1, title: chapterMatch.title || `Chapter ${chapters.length + 1}`, startLine: i, content: '', wordCount: 0, scenes: [] } currentContent = [] } else if (currentChapter) { currentContent.push(line) // Detect scene breaks if (this.patterns.sceneBreak.test(line)) { currentChapter.scenes.push({ startLine: i, endLine: i }) } } } // Save last chapter if (currentChapter) { currentChapter.content = currentContent.join('\n') currentChapter.wordCount = this.countWords(currentChapter.content) chapters.push(currentChapter) } // If no chapters detected, treat as single chapter if (chapters.length === 0) { chapters.push({ number: 1, title: 'Full Manuscript', startLine: 0, content: content, wordCount: this.countWords(content), scenes: [] }) } return { chapters, fullText: content, wordCount: this.countWords(content), metadata: { hasChapterTitles: chapters.some(ch => ch.title !== `Chapter ${ch.number}`), avgChapterLength: Math.round(this.countWords(content) / chapters.length), totalScenes: chapters.reduce((sum, ch) => sum + ch.scenes.length, 0) } } } private detectChapter(line: string): ChapterMatch | null { // Try numbered chapter with title const namedMatch = line.match(this.patterns.named) if (namedMatch) { return { type: 'named', number: namedMatch[2], title: namedMatch[3].trim() } } // Try numbered chapter const numberedMatch = line.match(this.patterns.numbered) if (numberedMatch) { return { type: 'numbered', number: numberedMatch[2], title: null } } return null } private countWords(text: string): number { return text.split(/\s+/).filter(word => word.length > 0).length } }

3. Analysis Dimensions

// packages/manuscript-analysis/src/analyzers/dimension-analyzers.ts import { AIService } from '../services/ai-service' export class DimensionAnalyzers { constructor(private aiService: AIService) {} async analyzeCharacterDevelopment(chapters: Chapter[]): Promise<CharacterAnalysis> { const characterPrompt = ` Analyze character development across these chapters: 1. Identify main characters and their arcs 2. Evaluate character consistency 3. Assess character motivations and goals 4. Analyze character relationships 5. Identify character growth moments Return structured JSON with scores and detailed feedback. ` const results = await Promise.all( chapters.map(chapter => this.aiService.analyzeManuscript(chapter.content, 'character', { chapterNumber: chapter.number }) ) ) return this.aggregateCharacterAnalysis(results) } async analyzePlotStructure(structure: ManuscriptStructure): Promise<PlotAnalysis> { const plotPrompt = ` Analyze the plot structure: 1. Identify key plot points (inciting incident, climax, resolution) 2. Evaluate pacing and tension curves 3. Assess subplot integration 4. Identify plot holes or inconsistencies 5. Analyze story arc completeness Consider three-act structure and genre conventions. ` const analysis = await this.aiService.analyzeManuscript( structure.fullText, 'plot', { maxTokens: 6000 } ) return { ...analysis, structure: this.mapToThreeActStructure(analysis), tensionCurve: this.calculateTensionCurve(structure.chapters) } } async analyzeWritingStyle(content: string, genre: string): Promise<StyleAnalysis> { const stylePrompt = ` Analyze writing style for ${genre} genre: 1. Voice consistency and appropriateness 2. Prose quality and readability 3. Descriptive language effectiveness 4. Dialogue naturalness 5. Genre convention adherence 6. Show vs. tell balance Provide specific examples and improvement suggestions. ` return this.aiService.analyzeManuscript(content, 'style', { genre }) } async analyzeDialogue(chapters: Chapter[]): Promise<DialogueAnalysis> { const dialogueAnalyses = await Promise.all( chapters.map(async chapter => { const dialogueExtract = this.extractDialogue(chapter.content) if (dialogueExtract.length === 0) { return null } return this.aiService.analyzeManuscript( dialogueExtract, 'dialogue', { chapterNumber: chapter.number } ) }) ) return { overall: this.aggregateDialogueScores(dialogueAnalyses), byCharacter: this.analyzeDialogueByCharacter(chapters), suggestions: this.generateDialogueSuggestions(dialogueAnalyses) } } async analyzePacing(structure: ManuscriptStructure): Promise<PacingAnalysis> { const pacingData = structure.chapters.map(chapter => ({ chapterNumber: chapter.number, wordCount: chapter.wordCount, sceneCount: chapter.scenes.length, dialogueRatio: this.calculateDialogueRatio(chapter.content), actionRatio: this.calculateActionRatio(chapter.content) })) const analysis = await this.aiService.analyzeManuscript( JSON.stringify(pacingData), 'pacing', { analysisContext: 'chapter-metrics', genre: structure.metadata.genre } ) return { ...analysis, visualData: { chapterLengths: pacingData.map(p => p.wordCount), sceneCounts: pacingData.map(p => p.sceneCount), pacingScore: this.calculatePacingScore(pacingData) } } } private extractDialogue(content: string): string { // Extract dialogue using regex patterns const dialoguePattern = /"([^"]+)"/g const matches = content.match(dialoguePattern) || [] return matches.join('\n') } private calculateDialogueRatio(content: string): number { const dialogue = this.extractDialogue(content) return dialogue.length / content.length } private calculateActionRatio(content: string): number { // Simple heuristic: count action verbs const actionVerbs = ['ran', 'jumped', 'fought', 'grabbed', 'pushed', 'pulled'] const words = content.toLowerCase().split(/\s+/) const actionCount = words.filter(w => actionVerbs.includes(w)).length return actionCount / words.length } }

4. Report Generation

// packages/manuscript-analysis/src/services/report-generator.ts import { PDFGenerator } from '@mystoryflow/pdf' import { EmailService } from '@mystoryflow/email' export class ReportGenerator { private pdfGenerator: PDFGenerator private emailService: EmailService async generateReport( analysis: ComprehensiveAnalysis, manuscript: ManuscriptMetadata ): Promise<AnalysisReport> { const report = { id: generateReportId(), manuscriptId: manuscript.id, generatedAt: new Date(), executiveSummary: this.generateExecutiveSummary(analysis), detailedAnalysis: this.formatDetailedAnalysis(analysis), scores: this.calculateScores(analysis), recommendations: this.generateRecommendations(analysis), nextSteps: this.suggestNextSteps(analysis, manuscript) } // Generate PDF version const pdfUrl = await this.pdfGenerator.generateFromTemplate('analysis-report', { report, manuscript, branding: { logo: '/assets/mystoryflow-logo.png', colors: { primary: '#4F46E5', secondary: '#7C3AED' } } }) report.pdfUrl = pdfUrl return report } private generateExecutiveSummary(analysis: ComprehensiveAnalysis): string { const strengths = this.identifyTopStrengths(analysis) const weaknesses = this.identifyTopWeaknesses(analysis) const overallScore = this.calculateOverallScore(analysis) return ` Your manuscript shows ${this.getScoreDescription(overallScore)} overall quality. Key Strengths: ${strengths.map(s => `• ${s}`).join('\n')} Areas for Improvement: ${weaknesses.map(w => `• ${w}`).join('\n')} ${this.getMarketPotentialSummary(analysis.specialized.marketAnalysis)} ` } private calculateScores(analysis: ComprehensiveAnalysis): ScoreBreakdown { return { overall: this.calculateOverallScore(analysis), character: analysis.overall.scores.character || 0, plot: analysis.overall.scores.plot || 0, writing: analysis.overall.scores.style || 0, dialogue: analysis.specialized.dialogueAnalysis?.overall.score || 0, pacing: analysis.overall.scores.pacing || 0, marketFit: analysis.specialized.marketAnalysis?.score || 0 } } private generateRecommendations(analysis: ComprehensiveAnalysis): Recommendation[] { const recommendations: Recommendation[] = [] // Character recommendations if (analysis.overall.scores.character < 0.7) { recommendations.push({ category: 'character', priority: 'high', title: 'Deepen Character Development', description: 'Your characters could benefit from more depth and complexity.', actionItems: [ 'Add more backstory and motivation', 'Create stronger character arcs', 'Develop unique character voices' ] }) } // Add more recommendation logic... return recommendations.sort((a, b) => this.getPriorityWeight(a.priority) - this.getPriorityWeight(b.priority) ) } }

5. API Integration

// apps/story-analyzer/src/app/api/manuscripts/[id]/analyze/route.ts import { NextRequest, NextResponse } from 'next/server' import { AnalysisOrchestrator } from '@mystoryflow/manuscript-analysis' import { withAuth } from '@mystoryflow/auth' import { getManuscript } from '@/lib/manuscripts' export async function POST( req: NextRequest, { params }: { params: { id: string } } ) { try { const session = await withAuth(req) if (!session) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } const manuscript = await getManuscript(params.id, session.user.id) if (!manuscript) { return NextResponse.json({ error: 'Manuscript not found' }, { status: 404 }) } // Check if user has available analyses const analysisQuota = await checkUserAnalysisQuota(session.user.id) if (!analysisQuota.available) { return NextResponse.json( { error: 'Analysis quota exceeded', upgradeUrl: '/pricing' }, { status: 402 } ) } const orchestrator = new AnalysisOrchestrator() const jobId = await orchestrator.analyzeManuscript({ manuscriptId: params.id, userId: session.user.id, organizationId: session.user.organizationId, content: manuscript.content, metadata: { title: manuscript.title, genre: manuscript.genre, wordCount: manuscript.wordCount, userTier: session.user.subscriptionTier } }) return NextResponse.json({ jobId, status: 'processing', estimatedTime: getEstimatedTime(manuscript.wordCount), trackingUrl: `/api/jobs/${jobId}/status` }) } catch (error) { console.error('Analysis initiation failed:', error) return NextResponse.json( { error: 'Failed to start analysis' }, { status: 500 } ) } }

Database Schema

-- Analysis jobs tracking CREATE TABLE analyzer.analysis_jobs ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), manuscript_id UUID REFERENCES analyzer.manuscripts(id), user_id UUID REFERENCES auth.users(id), organization_id UUID REFERENCES public.organizations(id), status VARCHAR(50) DEFAULT 'pending', progress INTEGER DEFAULT 0, progress_message TEXT, started_at TIMESTAMP, completed_at TIMESTAMP, error_message TEXT, result_id UUID, created_at TIMESTAMP DEFAULT NOW(), INDEX idx_jobs_user (user_id), INDEX idx_jobs_manuscript (manuscript_id), INDEX idx_jobs_status (status) ); -- Analysis results CREATE TABLE analyzer.analysis_results ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), manuscript_id UUID REFERENCES analyzer.manuscripts(id), job_id UUID REFERENCES analyzer.analysis_jobs(id), analysis_type VARCHAR(50), overall_score DECIMAL(3,2), scores JSONB, detailed_analysis JSONB, recommendations JSONB, report_url TEXT, created_at TIMESTAMP DEFAULT NOW(), INDEX idx_results_manuscript (manuscript_id), INDEX idx_results_type (analysis_type) ); -- Chapter analysis data CREATE TABLE analyzer.chapter_analyses ( id UUID PRIMARY KEY DEFAULT uuid_generate_v4(), manuscript_id UUID REFERENCES analyzer.manuscripts(id), chapter_number INTEGER, chapter_title TEXT, word_count INTEGER, analysis_data JSONB, scores JSONB, created_at TIMESTAMP DEFAULT NOW(), INDEX idx_chapter_manuscript (manuscript_id, chapter_number) );

MVP Acceptance Criteria

  • Manuscript structure parsing with chapter detection
  • Multi-dimensional AI analysis orchestration
  • Progress tracking and status updates
  • Batch processing for large manuscripts
  • Genre-specific analysis features
  • Comprehensive report generation
  • API endpoints for analysis initiation
  • Job queue integration
  • User notification system

Post-MVP Enhancements

  • Real-time analysis with streaming
  • Comparative analysis with published works
  • Revision tracking and improvement metrics
  • Collaborative analysis features
  • Custom analysis dimensions
  • Industry-specific templates
  • Multi-language support
  • Audio manuscript analysis

Implementation Time

  • Analysis Orchestrator: 1 day
  • Chapter Parser: 0.5 days
  • Dimension Analyzers: 1.5 days
  • Report Generator: 1 day
  • API Integration: 0.5 days
  • Testing: 1 day
  • Total: 5.5 days

Dependencies

  • F004 - AI Services (for analysis capabilities)
  • F005 - Document Upload (for manuscript content)
  • F006 - Content Extraction (for text processing)

Next Feature

After completion, proceed to F009-SCORING-EVALUATION for detailed scoring algorithms.