Skip to Content
📚 MyStoryFlow Docs — Your guide to preserving family stories
Story AnalyzerImplementation RequirementsF012 - AI Scoring Algorithms

F012 - AI Scoring Algorithms

Objective

Define computational methods for converting AI analysis responses into consistent, meaningful numerical scores (0-100) that can be compared across manuscripts and categories.

Core Scoring Principles

1. Standardization

  • All scores normalized to 0-100 scale
  • Consistent scoring methodology across all categories
  • Weighted category scores for overall manuscript score

2. Validation

  • AI response validation and error handling
  • Fallback scoring for incomplete analysis
  • Confidence scoring for reliability assessment

3. Comparability

  • Scores meaningful across different manuscripts
  • Genre-adjusted scoring where appropriate
  • Historical score tracking for improvement measurement

AI Response Processing Pipeline

1. Response Validation and Parsing

interface AIRawResponse { content: string; model: string; processingTime: number; tokenCount: number; } interface ParsedAnalysisResult { scores: Record<string, number>; feedback: Record<string, string>; examples: Record<string, string[]>; suggestions: string[]; confidence: number; isValid: boolean; errors: string[]; } class AIResponseProcessor { parseAndValidateResponse( rawResponse: AIRawResponse, category: string ): ParsedAnalysisResult { try { // 1. Parse JSON response const parsed = JSON.parse(rawResponse.content); // 2. Validate required fields const validation = this.validateResponseStructure(parsed, category); // 3. Normalize scores to 0-100 range const normalizedScores = this.normalizeScores(parsed.scores || {}); // 4. Extract and validate feedback const feedback = this.extractFeedback(parsed); // 5. Calculate confidence score const confidence = this.calculateConfidenceScore(parsed, validation); return { scores: normalizedScores, feedback, examples: parsed.examples || {}, suggestions: parsed.improvement_suggestions || [], confidence, isValid: validation.isValid, errors: validation.errors }; } catch (error) { return this.createErrorResult(category, error as Error); } } private validateResponseStructure( parsed: any, category: string ): { isValid: boolean; errors: string[] } { const errors: string[] = []; // Check for required score field if (!parsed.overall_score && !parsed[`overall_${category}_score`]) { errors.push(`Missing overall score for ${category}`); } // Validate score range const score = parsed.overall_score || parsed[`overall_${category}_score`]; if (score && (score < 0 || score > 100)) { errors.push(`Score ${score} out of valid range (0-100)`); } // Check for feedback sections if (!parsed.improvement_suggestions) { errors.push('Missing improvement suggestions'); } return { isValid: errors.length === 0, errors }; } private normalizeScores(scores: Record<string, any>): Record<string, number> { const normalized: Record<string, number> = {}; for (const [key, value] of Object.entries(scores)) { const numValue = Number(value); if (isNaN(numValue)) { normalized[key] = 50; // Default neutral score } else { // Clamp to 0-100 range normalized[key] = Math.max(0, Math.min(100, Math.round(numValue))); } } return normalized; } private calculateConfidenceScore( parsed: any, validation: { isValid: boolean; errors: string[] } ): number { let confidence = parsed.confidence_score || 85; // Default confidence // Reduce confidence for validation errors confidence -= validation.errors.length * 10; // Reduce confidence for missing expected fields if (!parsed.examples) confidence -= 5; if (!parsed.improvement_suggestions) confidence -= 10; // Boost confidence for comprehensive responses if (parsed.examples && Object.keys(parsed.examples).length > 2) confidence += 5; if (parsed.improvement_suggestions && parsed.improvement_suggestions.length > 3) confidence += 5; return Math.max(0, Math.min(100, confidence)); } }

2. Category-Specific Scoring Algorithms

Pacing Analysis Scoring

class PacingScoreCalculator { calculatePacingScore(aiResponse: ParsedAnalysisResult): CategoryScore { const weights = { sentence_variation: 0.25, paragraph_flow: 0.30, chapter_momentum: 0.25, overall_momentum: 0.20 }; const subscores = { sentence_variation: this.calculateSentenceVariationScore(aiResponse), paragraph_flow: this.calculateParagraphFlowScore(aiResponse), chapter_momentum: this.calculateChapterMomentumScore(aiResponse), overall_momentum: this.calculateOverallMomentumScore(aiResponse) }; // Calculate weighted average const overallScore = Object.entries(weights).reduce((sum, [key, weight]) => { return sum + (subscores[key] * weight); }, 0); return { overall: Math.round(overallScore), subscores, breakdown: this.generateScoreBreakdown(subscores, weights), confidence: aiResponse.confidence }; } private calculateSentenceVariationScore(response: ParsedAnalysisResult): number { const sentenceData = response.scores.sentence_variation || response.scores.overall_score; // Adjust score based on specific metrics if available if (response.scores.short_sentences_ratio !== undefined) { const shortRatio = response.scores.short_sentences_ratio; const longRatio = response.scores.long_sentences_ratio || 0; // Ideal ratios: 20-30% short, 10-20% long, rest medium const shortPenalty = Math.abs(shortRatio - 25) * 2; // Penalty for deviation from 25% const longPenalty = Math.abs(longRatio - 15) * 2; // Penalty for deviation from 15% return Math.max(0, sentenceData - shortPenalty - longPenalty); } return sentenceData || 50; } private calculateParagraphFlowScore(response: ParsedAnalysisResult): number { let baseScore = response.scores.paragraph_flow || response.scores.overall_score || 50; // Penalize for slow sections const slowSectionPenalty = (response.examples.slow_sections?.length || 0) * 5; baseScore -= slowSectionPenalty; // Reward good dialogue-to-narrative balance const dialogueRatio = response.scores.dialogue_narrative_ratio; if (dialogueRatio && dialogueRatio >= 0.3 && dialogueRatio <= 0.7) { baseScore += 5; // Bonus for balanced dialogue/narrative } return Math.max(0, Math.min(100, baseScore)); } }

Dialogue Analysis Scoring

class DialogueScoreCalculator { calculateDialogueScore(aiResponse: ParsedAnalysisResult): CategoryScore { const weights = { naturalness: 0.30, character_voice: 0.25, technical_craft: 0.25, subtext_depth: 0.20 }; const subscores = { naturalness: this.calculateNaturalnessScore(aiResponse), character_voice: this.calculateCharacterVoiceScore(aiResponse), technical_craft: this.calculateTechnicalCraftScore(aiResponse), subtext_depth: this.calculateSubtextScore(aiResponse) }; const overallScore = Object.entries(weights).reduce((sum, [key, weight]) => { return sum + (subscores[key] * weight); }, 0); return { overall: Math.round(overallScore), subscores, breakdown: this.generateScoreBreakdown(subscores, weights), confidence: aiResponse.confidence }; } private calculateTechnicalCraftScore(response: ParsedAnalysisResult): number { let baseScore = response.scores.technical_craft || 50; // Penalize for excessive adverb use in dialogue tags const adverbCount = response.scores.adverb_overuse_count || 0; const adverbPenalty = Math.min(20, adverbCount * 2); // 2 points per adverb, max 20 point penalty // Penalize for repetitive dialogue tags const repetitiveTagPenalty = (response.examples.repetitive_tags?.length || 0) * 3; // Reward appropriate "said" tag usage (should be 60-80% of tags) const saidRatio = response.scores.said_tag_ratio || 0.5; const saidBonus = (saidRatio >= 0.6 && saidRatio <= 0.8) ? 5 : 0; return Math.max(0, Math.min(100, baseScore - adverbPenalty - repetitiveTagPenalty + saidBonus)); } }

Character Development Scoring

class CharacterScoreCalculator { calculateCharacterScore(aiResponse: ParsedAnalysisResult): CategoryScore { const weights = { protagonist_arc: 0.35, motivation_clarity: 0.25, character_consistency: 0.25, relationship_dynamics: 0.15 }; const subscores = { protagonist_arc: this.calculateProtagonistArcScore(aiResponse), motivation_clarity: this.calculateMotivationScore(aiResponse), character_consistency: this.calculateConsistencyScore(aiResponse), relationship_dynamics: this.calculateRelationshipScore(aiResponse) }; const overallScore = Object.entries(weights).reduce((sum, [key, weight]) => { return sum + (subscores[key] * weight); }, 0); return { overall: Math.round(overallScore), subscores, breakdown: this.generateScoreBreakdown(subscores, weights), confidence: aiResponse.confidence }; } private calculateProtagonistArcScore(response: ParsedAnalysisResult): number { let baseScore = response.scores.protagonist_arc || 50; // Boost score for clear growth moments const growthMoments = response.examples.key_growth_moments?.length || 0; const growthBonus = Math.min(15, growthMoments * 3); // 3 points per growth moment, max 15 // Penalize for low change measurement const changeScore = response.scores.change_measurement || 50; const changePenalty = changeScore < 40 ? (40 - changeScore) : 0; return Math.max(0, Math.min(100, baseScore + growthBonus - changePenalty)); } private calculateConsistencyScore(response: ParsedAnalysisResult): number { let baseScore = response.scores.character_consistency || 50; // Heavy penalty for contradictions const contradictions = response.examples.contradictions_found?.length || 0; const contradictionPenalty = contradictions * 10; // 10 points per contradiction // Reward high individual consistency scores const physicalConsistency = response.scores.physical_consistency || 50; const personalityConsistency = response.scores.personality_consistency || 50; const speechConsistency = response.scores.speech_consistency || 50; const avgConsistency = (physicalConsistency + personalityConsistency + speechConsistency) / 3; return Math.max(0, Math.min(100, avgConsistency - contradictionPenalty)); } }

3. Overall Manuscript Scoring

class OverallScoreCalculator { // AutoCrit category weights (adjusted for our enhanced analysis) private readonly CATEGORY_WEIGHTS = { pacing: 0.15, dialogue: 0.15, character_development: 0.20, plot_structure: 0.20, pov_consistency: 0.10, strong_writing: 0.15, world_building: 0.05 }; calculateOverallScore(categoryScores: Record<string, CategoryScore>): OverallScore { let weightedSum = 0; let totalWeight = 0; const validCategories: Record<string, number> = {}; // Calculate weighted average of valid category scores for (const [category, weight] of Object.entries(this.CATEGORY_WEIGHTS)) { const categoryScore = categoryScores[category]; if (categoryScore && categoryScore.confidence > 50) { // Only include confident scores weightedSum += categoryScore.overall * weight; totalWeight += weight; validCategories[category] = categoryScore.overall; } } const overallScore = totalWeight > 0 ? Math.round(weightedSum / totalWeight) : 0; return { overall: overallScore, categoryBreakdown: validCategories, confidenceWeightedScore: this.calculateConfidenceWeightedScore(categoryScores), readinessLevel: this.determineReadinessLevel(overallScore), improvementPriorities: this.identifyImprovementPriorities(categoryScores) }; } private determineReadinessLevel(score: number): ReadinessLevel { if (score >= 85) { return { level: 'publication_ready', description: 'Manuscript is ready for publication with minimal revisions', timeToMarket: '2-4 weeks', nextSteps: ['Final proofreading', 'Cover design', 'Marketing preparation'] }; } else if (score >= 70) { return { level: 'revision_needed', description: 'Manuscript needs moderate revisions before publication', timeToMarket: '2-3 months', nextSteps: ['Address major weaknesses', 'Beta reader feedback', 'Professional editing'] }; } else if (score >= 50) { return { level: 'major_revision', description: 'Manuscript requires significant development work', timeToMarket: '6-12 months', nextSteps: ['Structural revisions', 'Character development', 'Plot refinement'] }; } else { return { level: 'needs_development', description: 'Manuscript needs fundamental restructuring', timeToMarket: '12+ months', nextSteps: ['Major rewrite', 'Writing craft improvement', 'Story structure work'] }; } } private identifyImprovementPriorities( categoryScores: Record<string, CategoryScore> ): ImprovementPriority[] { const priorities: ImprovementPriority[] = []; // Sort categories by score (lowest first) and weight (highest first) const sortedCategories = Object.entries(categoryScores) .map(([category, score]) => ({ category, score: score.overall, weight: this.CATEGORY_WEIGHTS[category] || 0.1, impact: score.overall * (this.CATEGORY_WEIGHTS[category] || 0.1) })) .sort((a, b) => a.impact - b.impact); // Lowest impact first = highest priority // Take top 3 improvement priorities for (let i = 0; i < Math.min(3, sortedCategories.length); i++) { const item = sortedCategories[i]; priorities.push({ category: item.category, currentScore: item.score, potentialImpact: this.calculatePotentialImpact(item.score, item.weight), priority: i + 1, estimatedEffort: this.estimateEffort(item.category, item.score) }); } return priorities; } }

4. Quality Assurance and Error Handling

class ScoreQualityAssurance { validateScoreConsistency( scores: Record<string, CategoryScore>, originalResponse: AIRawResponse ): QualityReport { const issues: string[] = []; const warnings: string[] = []; // Check for unrealistic score patterns const scoreValues = Object.values(scores).map(s => s.overall); const avgScore = scoreValues.reduce((a, b) => a + b, 0) / scoreValues.length; // Flag if all scores are suspiciously similar const scoreVariance = this.calculateVariance(scoreValues); if (scoreVariance < 25) { // Less than 5-point standard deviation warnings.push('Scores show unusually low variation - may indicate analysis limitations'); } // Flag impossibly perfect scores const perfectScores = scoreValues.filter(s => s >= 95).length; if (perfectScores > scoreValues.length * 0.5) { issues.push('Too many near-perfect scores - analysis may be overly generous'); } // Flag impossibly low scores across all categories const lowScores = scoreValues.filter(s => s <= 20).length; if (lowScores > scoreValues.length * 0.7) { issues.push('Too many very low scores - analysis may be overly harsh'); } return { isValid: issues.length === 0, issues, warnings, overallConfidence: this.calculateOverallConfidence(scores), recommendReanalysis: issues.length > 0 }; } private calculateOverallConfidence(scores: Record<string, CategoryScore>): number { const confidenceValues = Object.values(scores).map(s => s.confidence); return confidenceValues.reduce((a, b) => a + b, 0) / confidenceValues.length; } private calculateVariance(numbers: number[]): number { const mean = numbers.reduce((a, b) => a + b, 0) / numbers.length; const squaredDiffs = numbers.map(n => Math.pow(n - mean, 2)); return squaredDiffs.reduce((a, b) => a + b, 0) / numbers.length; } }

5. Score Interpretation and Reporting

interface ScoreInterpretation { grade: 'A+' | 'A' | 'B+' | 'B' | 'C+' | 'C' | 'D' | 'F'; description: string; percentile: number; // Compared to typical manuscripts actionRequired: string; } class ScoreInterpreter { interpretScore(score: number, category: string): ScoreInterpretation { if (score >= 90) { return { grade: 'A+', description: `Exceptional ${category} - professional quality`, percentile: 95, actionRequired: 'Maintain this strength' }; } else if (score >= 80) { return { grade: 'A', description: `Strong ${category} - above average`, percentile: 80, actionRequired: 'Minor refinements only' }; } else if (score >= 70) { return { grade: 'B+', description: `Good ${category} - meets standards`, percentile: 65, actionRequired: 'Some improvement beneficial' }; } else if (score >= 60) { return { grade: 'B', description: `Average ${category} - acceptable`, percentile: 50, actionRequired: 'Moderate improvement needed' }; } else if (score >= 50) { return { grade: 'C+', description: `Below average ${category}`, percentile: 35, actionRequired: 'Significant improvement needed' }; } else if (score >= 40) { return { grade: 'C', description: `Weak ${category} - needs work`, percentile: 20, actionRequired: 'Major improvement required' }; } else if (score >= 30) { return { grade: 'D', description: `Poor ${category} - substantial issues`, percentile: 10, actionRequired: 'Extensive revision needed' }; } else { return { grade: 'F', description: `Critical ${category} issues`, percentile: 5, actionRequired: 'Complete rework necessary' }; } } }

This comprehensive scoring system ensures that AI responses are converted into meaningful, consistent numerical scores that authors can understand and act upon, matching AutoCrit’s functionality while providing more detailed feedback.