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.