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.