We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/199-mcp/mcp-limitless'
If you have feedback or need assistance with the MCP directory API, please join our Discord server
import { Lifelog, LifelogContentNode } from './limitless-client.js';
// =============================================================================
// SCIENTIFICALLY VALIDATED SPEECH VITALITY INDEX
// =============================================================================
// Based on empirical analysis of Limitless transcription data
// Validated metrics only - no speculative biomarkers
export interface SegmentMetrics {
content: string;
duration: number;
wordCount: number;
wpm: number;
speakerIdentifier: string | null;
speakerName: string | null;
startOffset: number;
endOffset: number;
category: 'micro' | 'short' | 'medium' | 'long' | 'problematic';
}
export interface TurnTransition {
gapDuration: number;
fromSpeaker: string;
toSpeaker: string;
responseType: 'quick' | 'normal' | 'slow' | 'delayed';
}
export interface ConversationContext {
type: 'presentation' | 'discussion' | 'casual' | 'automated' | 'unknown';
confidence: number;
indicators: string[];
}
export interface EngagementMetrics {
// Micro-responsiveness (validated: 7-14% baseline, >20% = highly engaged)
microResponseCount: number;
microResponseRate: number; // micro responses / total segments
responsiveWordCount: number; // "yeah", "really?", "oh" etc.
responsiveWordRate: number; // responsive words / micro segments
// Turn-taking velocity (validated: 55% quick responses in active conversations)
totalTransitions: number;
quickResponseCount: number; // <500ms responses
quickResponseRatio: number; // quick / total responses
averageResponseTime: number; // mean gap after speaker change
medianResponseTime: number; // median gap after speaker change
}
export interface FluencyMetrics {
// Filtered WPM analysis (validated: 47-58% segments suitable, 100-250 WPM realistic)
validSegmentCount: number;
validSegmentRatio: number; // valid segments / total segments
meanWPM: number; // from segments ≥800ms, ≤30s, ≥5 words
medianWPM: number;
wpmStandardDeviation: number;
wpmConsistency: number; // 1 - (stddev / mean)
realisticWPMRatio: number; // segments in 100-250 WPM range
// Speech burst analysis
avgSegmentDuration: number;
medianSegmentDuration: number;
segmentDurationVariability: number;
}
export interface InteractionMetrics {
// Speaker dynamics
totalSpeakers: number;
speakerTransitions: number;
userSpeakingTime: number; // milliseconds
totalValidSpeakingTime: number; // milliseconds
userSpeakingRatio: number; // user time / total time
conversationBalance: number; // 1 - |0.5 - userSpeakingRatio|
// Conversational patterns
overlapsDetected: number; // negative gaps
longPauseCount: number; // gaps >3000ms
silenceRatio: number; // pause time / total time
}
export interface DataQualityMetrics {
totalSegments: number;
categorizedSegments: {
micro: number; // ≤100ms
short: number; // 100-800ms
medium: number; // 800-8000ms
long: number; // >8000ms
problematic: number; // invalid timing/content
};
anomalies: {
zeroLengthSegments: number;
negativeGaps: number;
suspiciouslyLongGaps: number; // >10s
unrealisticWPM: number; // >500 WPM
};
dataReliability: 'high' | 'medium' | 'low';
confidenceScore: number; // 0-100
}
export interface ValidatedSpeechVitality {
// Core validated metrics
engagement: EngagementMetrics;
fluency: FluencyMetrics;
interaction: InteractionMetrics;
// Context and quality
context: ConversationContext;
dataQuality: DataQualityMetrics;
// Composite scores (0-100)
overallScore: number;
engagementScore: number;
fluencyScore: number;
interactionScore: number;
// Analysis metadata
analysisTimestamp: Date;
conversationDuration: number; // milliseconds
analysisVersion: string;
}
export class ValidatedSpeechVitalityAnalyzer {
private static readonly RESPONSIVE_WORDS = /^(yeah|yes|okay|ok|really|oh|wow|huh|what|no|right|sure|exactly|absolutely|definitely|totally|mmm|uh|um|aha|mhm)\.?$/i;
private static readonly VERSION = "2.0.0-validated";
/**
* Main analysis entry point - scientifically validated approach
*/
static analyze(lifelogs: Lifelog[]): ValidatedSpeechVitality {
// Extract and categorize all segments
const allSegments = this.extractAllSegments(lifelogs);
const categorized = this.categorizeSegments(allSegments);
// Analyze each validated dimension
const engagement = this.analyzeEngagement(allSegments);
const fluency = this.analyzeFluency(categorized.medium.concat(categorized.long));
const interaction = this.analyzeInteraction(allSegments);
const context = this.detectContext(engagement, interaction, fluency);
const dataQuality = this.assessDataQuality(categorized, allSegments);
// Calculate composite scores
const scores = this.calculateCompositeScores(engagement, fluency, interaction, dataQuality);
return {
engagement,
fluency,
interaction,
context,
dataQuality,
overallScore: scores.overall,
engagementScore: scores.engagement,
fluencyScore: scores.fluency,
interactionScore: scores.interaction,
analysisTimestamp: new Date(),
conversationDuration: this.calculateTotalDuration(lifelogs),
analysisVersion: this.VERSION
};
}
/**
* Extract all segments from lifelogs with proper validation
*/
private static extractAllSegments(lifelogs: Lifelog[]): SegmentMetrics[] {
const segments: SegmentMetrics[] = [];
for (const lifelog of lifelogs) {
if (!lifelog.contents) continue;
for (const node of lifelog.contents) {
if (!node.content || node.startOffsetMs === undefined || node.endOffsetMs === undefined) {
continue;
}
const duration = node.endOffsetMs - node.startOffsetMs;
const content = node.content.trim();
const wordCount = content.split(/\s+/).filter(w => w.length > 0).length;
const wpm = wordCount > 0 && duration > 0 ? (wordCount / (duration / 60000)) : 0;
segments.push({
content,
duration,
wordCount,
wpm,
speakerIdentifier: node.speakerIdentifier || null,
speakerName: node.speakerName || null,
startOffset: node.startOffsetMs,
endOffset: node.endOffsetMs,
category: this.categorizeSegment(duration, wordCount)
});
}
}
return segments.sort((a, b) => a.startOffset - b.startOffset);
}
/**
* Categorize individual segment by duration and content
*/
private static categorizeSegment(duration: number, wordCount: number): 'micro' | 'short' | 'medium' | 'long' | 'problematic' {
if (duration <= 0) return 'problematic';
if (duration <= 100) return 'micro';
if (duration <= 800) return 'short';
if (duration <= 8000) return 'medium';
return 'long';
}
/**
* Group segments by category for analysis
*/
private static categorizeSegments(segments: SegmentMetrics[]) {
return {
micro: segments.filter(s => s.category === 'micro'),
short: segments.filter(s => s.category === 'short'),
medium: segments.filter(s => s.category === 'medium'),
long: segments.filter(s => s.category === 'long'),
problematic: segments.filter(s => s.category === 'problematic')
};
}
/**
* Analyze engagement through micro-responsiveness and turn-taking
* Validated: 7-14% micro baseline, >20% = highly engaged
*/
private static analyzeEngagement(segments: SegmentMetrics[]): EngagementMetrics {
const microSegments = segments.filter(s => s.category === 'micro');
const responsiveWords = microSegments.filter(s =>
this.RESPONSIVE_WORDS.test(s.content)
);
// Calculate turn transitions
const transitions = this.calculateTurnTransitions(segments);
const quickResponses = transitions.filter(t => t.responseType === 'quick');
const totalTransitions = transitions.length;
const avgResponseTime = totalTransitions > 0
? transitions.reduce((sum, t) => sum + t.gapDuration, 0) / totalTransitions
: 0;
const medianResponseTime = this.median(transitions.map(t => t.gapDuration));
return {
microResponseCount: microSegments.length,
microResponseRate: segments.length > 0 ? microSegments.length / segments.length : 0,
responsiveWordCount: responsiveWords.length,
responsiveWordRate: microSegments.length > 0 ? responsiveWords.length / microSegments.length : 0,
totalTransitions,
quickResponseCount: quickResponses.length,
quickResponseRatio: totalTransitions > 0 ? quickResponses.length / totalTransitions : 0,
averageResponseTime: avgResponseTime,
medianResponseTime
};
}
/**
* Analyze fluency using filtered, validated segments only
* Validated: 47-58% segments suitable, 100-250 WPM realistic
*/
private static analyzeFluency(validSegments: SegmentMetrics[]): FluencyMetrics {
// Filter for WPM calculation: ≥800ms, ≤30s, ≥5 words, reasonable WPM
const wpmCandidates = validSegments.filter(s =>
s.duration >= 800 &&
s.duration <= 30000 &&
s.wordCount >= 5 &&
s.wpm > 0
);
const wpmValues = wpmCandidates.map(s => s.wpm);
const realisticWPM = wpmValues.filter(wpm => wpm >= 100 && wpm <= 250);
const meanWPM = this.mean(wpmValues);
const medianWPM = this.median(wpmValues);
const wpmStdDev = this.standardDeviation(wpmValues);
const durations = validSegments.map(s => s.duration);
const avgDuration = this.mean(durations);
const medianDuration = this.median(durations);
const durationVariability = durations.length > 0 ? this.standardDeviation(durations) / avgDuration : 0;
return {
validSegmentCount: wpmCandidates.length,
validSegmentRatio: validSegments.length > 0 ? wpmCandidates.length / validSegments.length : 0,
meanWPM,
medianWPM,
wpmStandardDeviation: wpmStdDev,
wpmConsistency: meanWPM > 0 ? Math.max(0, 1 - (wpmStdDev / meanWPM)) : 0,
realisticWPMRatio: wpmValues.length > 0 ? realisticWPM.length / wpmValues.length : 0,
avgSegmentDuration: avgDuration,
medianSegmentDuration: medianDuration,
segmentDurationVariability: durationVariability
};
}
/**
* Analyze interaction patterns and conversational dynamics
*/
private static analyzeInteraction(segments: SegmentMetrics[]): InteractionMetrics {
const speakers = new Set(segments.map(s => s.speakerIdentifier || s.speakerName || 'unknown'));
const transitions = this.calculateTurnTransitions(segments);
// Calculate speaking times
const userSegments = segments.filter(s =>
s.speakerIdentifier === 'user' || s.speakerName === 'You'
);
const userSpeakingTime = userSegments.reduce((sum, s) => sum + s.duration, 0);
const totalSpeakingTime = segments.reduce((sum, s) => sum + s.duration, 0);
// Detect anomalies
const overlaps = transitions.filter(t => t.gapDuration < 0).length;
const longPauses = transitions.filter(t => t.gapDuration > 3000).length;
// Calculate silence ratio (gaps vs speaking time)
const totalGapTime = transitions.reduce((sum, t) => sum + Math.max(0, t.gapDuration), 0);
const silenceRatio = totalSpeakingTime > 0 ? totalGapTime / (totalSpeakingTime + totalGapTime) : 0;
const userSpeakingRatio = totalSpeakingTime > 0 ? userSpeakingTime / totalSpeakingTime : 0;
const conversationBalance = 1 - Math.abs(0.5 - userSpeakingRatio);
return {
totalSpeakers: speakers.size,
speakerTransitions: transitions.length,
userSpeakingTime,
totalValidSpeakingTime: totalSpeakingTime,
userSpeakingRatio,
conversationBalance,
overlapsDetected: overlaps,
longPauseCount: longPauses,
silenceRatio
};
}
/**
* Calculate turn transitions between speakers
*/
private static calculateTurnTransitions(segments: SegmentMetrics[]): TurnTransition[] {
const transitions: TurnTransition[] = [];
for (let i = 1; i < segments.length; i++) {
const prev = segments[i - 1];
const current = segments[i];
const prevSpeaker = prev.speakerIdentifier || prev.speakerName || 'unknown';
const currentSpeaker = current.speakerIdentifier || current.speakerName || 'unknown';
// Only count actual speaker changes
if (prevSpeaker !== currentSpeaker) {
const gap = current.startOffset - prev.endOffset;
let responseType: 'quick' | 'normal' | 'slow' | 'delayed';
if (gap < 500) responseType = 'quick';
else if (gap <= 1500) responseType = 'normal';
else if (gap <= 3000) responseType = 'slow';
else responseType = 'delayed';
transitions.push({
gapDuration: gap,
fromSpeaker: prevSpeaker,
toSpeaker: currentSpeaker,
responseType
});
}
}
return transitions;
}
/**
* Detect conversation context based on validated patterns
*/
private static detectContext(
engagement: EngagementMetrics,
interaction: InteractionMetrics,
fluency: FluencyMetrics
): ConversationContext {
const indicators: string[] = [];
let type: ConversationContext['type'] = 'unknown';
let confidence = 0;
// Automated detection (e.g., elevator announcements)
if (engagement.microResponseRate < 0.05 && interaction.speakerTransitions < 3) {
type = 'automated';
confidence = 0.9;
indicators.push('No responsiveness', 'Minimal speaker changes');
}
// Presentation detection
else if (engagement.quickResponseRatio < 0.1 && interaction.userSpeakingRatio < 0.2) {
type = 'presentation';
confidence = 0.8;
indicators.push('Few quick responses', 'One-sided speaking');
}
// Active discussion
else if (engagement.quickResponseRatio > 0.4 && interaction.speakerTransitions > 20) {
type = 'discussion';
confidence = 0.85;
indicators.push('High responsiveness', 'Frequent speaker changes');
}
// Casual conversation
else if (engagement.microResponseRate > 0.15 && interaction.conversationBalance > 0.3) {
type = 'casual';
confidence = 0.7;
indicators.push('Good responsiveness', 'Balanced participation');
}
return { type, confidence, indicators };
}
/**
* Assess overall data quality and reliability
*/
private static assessDataQuality(
categorized: ReturnType<typeof ValidatedSpeechVitalityAnalyzer.categorizeSegments>,
allSegments: SegmentMetrics[]
): DataQualityMetrics {
const total = allSegments.length;
const anomalies = {
zeroLengthSegments: allSegments.filter(s => s.duration <= 0).length,
negativeGaps: 0, // Will be calculated in turn transitions
suspiciouslyLongGaps: 0, // Will be calculated in turn transitions
unrealisticWPM: allSegments.filter(s => s.wpm > 500).length
};
// Calculate reliability based on usable data percentage
const usableSegments = categorized.medium.length + categorized.long.length;
const usableRatio = total > 0 ? usableSegments / total : 0;
let dataReliability: 'high' | 'medium' | 'low';
let confidenceScore: number;
if (usableRatio > 0.6 && anomalies.unrealisticWPM / total < 0.1) {
dataReliability = 'high';
confidenceScore = 85;
} else if (usableRatio > 0.4 && anomalies.unrealisticWPM / total < 0.2) {
dataReliability = 'medium';
confidenceScore = 65;
} else {
dataReliability = 'low';
confidenceScore = 35;
}
return {
totalSegments: total,
categorizedSegments: {
micro: categorized.micro.length,
short: categorized.short.length,
medium: categorized.medium.length,
long: categorized.long.length,
problematic: categorized.problematic.length
},
anomalies,
dataReliability,
confidenceScore
};
}
/**
* Calculate composite scores (0-100) from validated metrics
*/
private static calculateCompositeScores(
engagement: EngagementMetrics,
fluency: FluencyMetrics,
interaction: InteractionMetrics,
dataQuality: DataQualityMetrics
) {
// Engagement score (0-100)
const engagementScore = Math.round(
(engagement.microResponseRate * 40) + // High responsiveness
(engagement.quickResponseRatio * 30) + // Quick responses
(Math.min(1, engagement.responsiveWordRate) * 30) // Responsive words
) * 100;
// Fluency score (0-100)
const wpmNormalized = fluency.meanWPM > 0
? Math.min(1, Math.max(0, (fluency.meanWPM - 100) / 150)) // 100-250 WPM range
: 0;
const fluencyScore = Math.round(
(wpmNormalized * 50) + // Appropriate speaking rate
(fluency.wpmConsistency * 30) + // Consistent delivery
(fluency.realisticWPMRatio * 20) // Reliable measurements
) * 100;
// Interaction score (0-100)
const interactionScore = Math.round(
(interaction.conversationBalance * 40) + // Balanced participation
(Math.min(1, interaction.speakerTransitions / 50) * 30) + // Active dialogue
(Math.max(0, 1 - interaction.silenceRatio) * 30) // Good flow
) * 100;
// Overall score weighted by data quality
const rawOverall = (engagementScore * 0.4) + (fluencyScore * 0.35) + (interactionScore * 0.25);
const qualityMultiplier = dataQuality.confidenceScore / 100;
const overallScore = Math.round(rawOverall * qualityMultiplier);
return {
engagement: Math.min(100, Math.max(0, engagementScore)),
fluency: Math.min(100, Math.max(0, fluencyScore)),
interaction: Math.min(100, Math.max(0, interactionScore)),
overall: Math.min(100, Math.max(0, overallScore))
};
}
/**
* Calculate total conversation duration from lifelogs
*/
private static calculateTotalDuration(lifelogs: Lifelog[]): number {
return lifelogs.reduce((total, log) => {
const start = new Date(log.startTime).getTime();
const end = new Date(log.endTime).getTime();
return total + (end - start);
}, 0);
}
// Utility functions
private static mean(values: number[]): number {
return values.length > 0 ? values.reduce((a, b) => a + b, 0) / values.length : 0;
}
private static median(values: number[]): number {
if (values.length === 0) return 0;
const sorted = [...values].sort((a, b) => a - b);
const mid = Math.floor(sorted.length / 2);
return sorted.length % 2 ? sorted[mid] : (sorted[mid - 1] + sorted[mid]) / 2;
}
private static standardDeviation(values: number[]): number {
if (values.length === 0) return 0;
const mean = this.mean(values);
const squaredDiffs = values.map(v => Math.pow(v - mean, 2));
return Math.sqrt(this.mean(squaredDiffs));
}
}