UNPKG

aiwg

Version:

Deployment tool and support utility for AI context. Copies agents, skills, commands, rules, and behaviors into the paths each AI platform reads (Claude Code, Codex, Copilot, Cursor, Warp, OpenClaw, and 6 more) so one source of truth works across 10 platfo

439 lines 18.1 kB
/** * Voice Analyzer * * Analyzes content voice characteristics, detects voice type, * and scores diversity between variations. */ import { getScoringConfig } from './scoring-config-loader.js'; /** * Voice Analyzer for content characteristics */ export class VoiceAnalyzer { voicePatterns; constructor() { this.voicePatterns = this.initializeVoicePatterns(); } /** * Analyze voice characteristics of content */ analyzeVoice(content) { const scores = this.scoreVoices(content); const markers = this.detectMarkers(content); const perspective = this.detectPerspective(content); const tone = this.detectTone(content); const config = getScoringConfig(); const sortedScores = Object.entries(scores).sort((a, b) => b[1] - a[1]); const primaryVoice = sortedScores[0][1] > sortedScores[1][1] * config.voiceDetection.mixedVoiceThreshold ? sortedScores[0][0] : 'mixed'; const confidence = primaryVoice === 'mixed' ? config.voiceDetection.defaultMixedConfidence : Math.round((sortedScores[0][1] / (sortedScores[0][1] + sortedScores[1][1])) * 100); const wordCount = this.countWords(content); const sentenceCount = this.countSentences(content); return { primaryVoice, confidence, characteristics: { academic: scores.academic, technical: scores.technical, executive: scores.executive, casual: scores.casual, }, markers, perspective, tone, metadata: { wordCount, sentenceCount, averageSentenceLength: sentenceCount > 0 ? Math.round(wordCount / sentenceCount) : 0, }, }; } /** * Detect primary voice of content */ detectVoice(content) { const profile = this.analyzeVoice(content); return profile.primaryVoice; } /** * Score diversity between multiple variations */ scoreDiversity(variations) { if (variations.length < 2) { return 0; } let totalDiversity = 0; let comparisons = 0; // Compare each pair of variations for (let i = 0; i < variations.length; i++) { for (let j = i + 1; j < variations.length; j++) { const comparison = this.compareVariations(variations[i], variations[j]); totalDiversity += (100 - comparison.similarity); comparisons++; } } return Math.round(totalDiversity / comparisons); } /** * Compare two variations and identify differences */ compareVariations(original, variation) { const originalProfile = this.analyzeVoice(original); const variationProfile = this.analyzeVoice(variation); const similarity = this.calculateSimilarity(original, variation); const differences = this.identifyDifferences(original, variation, originalProfile, variationProfile); const structuralChanges = this.detectStructuralChanges(original, variation); const voiceDiff = Math.abs(originalProfile.characteristics[originalProfile.primaryVoice === 'mixed' ? 'casual' : originalProfile.primaryVoice] - variationProfile.characteristics[variationProfile.primaryVoice === 'mixed' ? 'casual' : variationProfile.primaryVoice]); return { similarity, differences, voiceShift: { from: originalProfile.primaryVoice, to: variationProfile.primaryVoice, magnitude: Math.round(voiceDiff), }, structuralChanges, }; } /** * Detect perspective used in content */ detectPerspective(content) { const firstPersonCount = (content.match(/\b(I|me|my|we|us|our)\b/gi) || []).length; const thirdPersonCount = (content.match(/\b(he|she|they|their|one)\b/gi) || []).length; const impersonalCount = (content.match(/\b(the system|the approach|users|developers)\b/gi) || []).length; if (firstPersonCount > thirdPersonCount && firstPersonCount > impersonalCount) { return 'first-person'; } if (thirdPersonCount > firstPersonCount && thirdPersonCount > impersonalCount) { return 'third-person'; } return 'neutral'; } /** * Detect tone of content */ detectTone(content) { const formalMarkers = (content.match(/\b(furthermore|moreover|nevertheless|consequently|demonstrates|efficacy|implementation)\b/gi) || []).length; const casualMarkers = (content.match(/\b(just|really|pretty|basically|great|awesome|cool)\b/gi) || []).length; const enthusiasticMarkers = (content.match(/!/g) || []).length + (content.match(/\b(amazing|fantastic|excellent|brilliant)\b/gi) || []).length; const contractions = (content.match(/\b(don't|can't|won't|it's|that's|we're|you're|I'm)\b/gi) || []).length; // Enthusiastic: multiple exclamations or enthusiastic words if (enthusiasticMarkers > 1) { return 'enthusiastic'; } // Conversational: has contractions and casual words, or contractions with enthusiasm if (contractions > 0 && (casualMarkers > 0 || enthusiasticMarkers > 0)) { return 'conversational'; } // Formal: has formal markers and no contractions if (formalMarkers > 0 && contractions === 0) { return 'formal'; } return 'matter-of-fact'; } /** * Analyze voice consistency across content sections */ analyzeConsistency(content, sectionSize = 200) { const sections = this.splitIntoSections(content, sectionSize); const sectionProfiles = sections.map(section => this.analyzeVoice(section)); if (sectionProfiles.length < 2) { return { overallConsistency: 100, sectionProfiles, inconsistencies: [], }; } // Check voice consistency const primaryVoices = sectionProfiles.map(p => p.primaryVoice); const dominantVoice = this.findMostCommon(primaryVoices); const consistentSections = primaryVoices.filter(v => v === dominantVoice).length; const consistency = Math.round((consistentSections / primaryVoices.length) * 100); // Identify inconsistencies const inconsistencies = []; for (let i = 0; i < sectionProfiles.length; i++) { if (sectionProfiles[i].primaryVoice !== dominantVoice) { inconsistencies.push(`Section ${i + 1}: ${sectionProfiles[i].primaryVoice} (expected ${dominantVoice})`); } } return { overallConsistency: consistency, sectionProfiles, inconsistencies, }; } // Private helper methods initializeVoicePatterns() { const patterns = new Map(); patterns.set('academic', { strongMarkers: [ /\([\w\s,&]+,\s*\d{4}\)/, // Citations /\b(furthermore|moreover|nevertheless|consequently)\b/i, /\b(suggests? that|appears? to|may indicate)\b/i, /\b(research|study|analysis|empirical)\b/i, ], moderateMarkers: [ /\b(however|although|while)\b/i, /\b(demonstrate|illustrate|indicate)\b/i, /\b(approach|methodology|framework)\b/i, ], weakMarkers: [ /\b(consider|examine|explore)\b/i, ], }); patterns.set('technical', { strongMarkers: [ /\d+(\.\d+)?\s*(ms|MB|GB|KB|%|seconds)/, // Metrics /\b(latency|throughput|payload|optimization)\b/i, /\b(implementation|configuration|deployment)\b/i, /```[\s\S]*?```/, // Code blocks ], moderateMarkers: [ /\b(system|architecture|performance|scalability)\b/i, /\b(API|HTTP|TCP|SQL|REST)\b/, /\b(cache|buffer|queue|pool)\b/i, ], weakMarkers: [ /\b(code|function|method|class)\b/i, ], }); patterns.set('executive', { strongMarkers: [ /\$[\d,]+[KMB]?/, // Dollar amounts /\d+%\s*(increase|decrease|improvement|growth|reduction)/i, /\b(ROI|revenue|cost savings|profit)\b/i, /\b(strategic|leverage|maximize|optimize)\b/i, ], moderateMarkers: [ /\b(recommend|priority|decision|initiative)\b/i, /\b(stakeholder|business impact|value proposition)\b/i, /\bQ[1-4]\b/, // Quarters ], weakMarkers: [ /\b(advantage|benefit|opportunity|risk)\b/i, ], }); patterns.set('casual', { strongMarkers: [ /\b(don't|can't|won't|it's|that's|we're|you're)\b/, // Contractions /\b(here's the thing|look|basically|pretty much)\b/i, /\b(like|you know|I mean)\b/i, ], moderateMarkers: [ /\b(just|really|very|pretty|quite)\b/i, /\b(cool|nice|great|awesome)\b/i, /[.!?]\s+([A-Z])/, // Sentence fragments ], weakMarkers: [ /\b(thing|stuff|kind of|sort of)\b/i, ], }); return patterns; } scoreVoices(content) { const scores = { academic: 0, technical: 0, executive: 0, casual: 0, }; const config = getScoringConfig(); const weights = config.voiceDetection.markerWeights; for (const [voice, patterns] of this.voicePatterns.entries()) { // Strong markers for (const pattern of patterns.strongMarkers) { // Preserve original flags and add 'g' for global matching const flags = pattern.flags.includes('g') ? pattern.flags : pattern.flags + 'g'; const matches = content.match(new RegExp(pattern.source, flags)); if (matches) { scores[voice] += matches.length * weights.strong; } } // Moderate markers for (const pattern of patterns.moderateMarkers) { const flags = pattern.flags.includes('g') ? pattern.flags : pattern.flags + 'g'; const matches = content.match(new RegExp(pattern.source, flags)); if (matches) { scores[voice] += matches.length * weights.moderate; } } // Weak markers for (const pattern of patterns.weakMarkers) { const flags = pattern.flags.includes('g') ? pattern.flags : pattern.flags + 'g'; const matches = content.match(new RegExp(pattern.source, flags)); if (matches) { scores[voice] += matches.length * weights.weak; } } } return scores; } detectMarkers(content) { const markers = []; for (const [voice, patterns] of this.voicePatterns.entries()) { // Strong markers for (const pattern of patterns.strongMarkers) { const regex = new RegExp(pattern, 'gi'); let match; while ((match = regex.exec(content)) !== null) { markers.push({ type: voice, text: match[0], position: match.index, strength: 'strong', }); } } // Moderate markers (sample first 3) for (const pattern of patterns.moderateMarkers.slice(0, 3)) { const regex = new RegExp(pattern, 'gi'); let match; while ((match = regex.exec(content)) !== null) { markers.push({ type: voice, text: match[0], position: match.index, strength: 'moderate', }); } } } return markers.sort((a, b) => a.position - b.position); } calculateSimilarity(str1, str2) { // Use Levenshtein distance const distance = this.levenshteinDistance(str1, str2); const maxLength = Math.max(str1.length, str2.length); return Math.round((1 - distance / maxLength) * 100); } levenshteinDistance(str1, str2) { const matrix = []; for (let i = 0; i <= str2.length; i++) { matrix[i] = [i]; } for (let j = 0; j <= str1.length; j++) { matrix[0][j] = j; } for (let i = 1; i <= str2.length; i++) { for (let j = 1; j <= str1.length; j++) { if (str2.charAt(i - 1) === str1.charAt(j - 1)) { matrix[i][j] = matrix[i - 1][j - 1]; } else { matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1); } } } return matrix[str2.length][str1.length]; } identifyDifferences(original, variation, originalProfile, variationProfile) { const differences = []; // Voice change if (originalProfile.primaryVoice !== variationProfile.primaryVoice) { differences.push({ type: 'modification', description: `Voice changed from ${originalProfile.primaryVoice} to ${variationProfile.primaryVoice}`, impact: 'high', }); } // Perspective change if (originalProfile.perspective !== variationProfile.perspective) { differences.push({ type: 'modification', description: `Perspective shifted from ${originalProfile.perspective} to ${variationProfile.perspective}`, impact: 'medium', }); } // Tone change if (originalProfile.tone !== variationProfile.tone) { differences.push({ type: 'modification', description: `Tone adjusted from ${originalProfile.tone} to ${variationProfile.tone}`, impact: 'medium', }); } // Length change const lengthDiff = Math.abs(original.length - variation.length); if (lengthDiff > original.length * 0.2) { differences.push({ type: original.length > variation.length ? 'removal' : 'addition', description: `Content length ${original.length > variation.length ? 'reduced' : 'expanded'} by ${Math.round((lengthDiff / original.length) * 100)}%`, impact: 'medium', }); } // Sentence structure change const originalSentences = this.countSentences(original); const variationSentences = this.countSentences(variation); if (Math.abs(originalSentences - variationSentences) > 2) { differences.push({ type: 'modification', description: `Sentence count changed from ${originalSentences} to ${variationSentences}`, impact: 'low', }); } return differences; } detectStructuralChanges(original, variation) { const changes = []; const originalBullets = (original.match(/^[-*]\s/gm) || []).length; const variationBullets = (variation.match(/^[-*]\s/gm) || []).length; if (originalBullets === 0 && variationBullets > 0) { changes.push('Converted to bullet point format'); } else if (originalBullets > 0 && variationBullets === 0) { changes.push('Converted from bullet points to narrative'); } const originalHeadings = (original.match(/^#{1,6}\s/gm) || []).length; const variationHeadings = (variation.match(/^#{1,6}\s/gm) || []).length; if (variationHeadings > originalHeadings) { changes.push('Added section headings'); } const originalQuestions = (original.match(/\?/g) || []).length; const variationQuestions = (variation.match(/\?/g) || []).length; if (variationQuestions > originalQuestions * 2) { changes.push('Converted to Q&A format'); } const originalCode = (original.match(/```/g) || []).length; const variationCode = (variation.match(/```/g) || []).length; if (variationCode > originalCode) { changes.push('Added code examples'); } return changes; } countWords(content) { return content.split(/\s+/).filter(word => word.length > 0).length; } countSentences(content) { return content.split(/[.!?]+/).filter(s => s.trim().length > 0).length; } splitIntoSections(content, sectionSize) { // Filter out empty strings from split const words = content.split(/\s+/).filter(w => w.length > 0); const sections = []; for (let i = 0; i < words.length; i += sectionSize) { sections.push(words.slice(i, i + sectionSize).join(' ')); } return sections; } findMostCommon(items) { const counts = new Map(); for (const item of items) { counts.set(item, (counts.get(item) || 0) + 1); } let maxCount = 0; let mostCommon = items[0]; for (const [item, count] of counts.entries()) { if (count > maxCount) { maxCount = count; mostCommon = item; } } return mostCommon; } } //# sourceMappingURL=voice-analyzer.js.map