@aituber-onair/manneri 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +609 -0
- package/README.md +600 -0
- package/dist/analyzers/KeywordExtractor.d.ts +48 -0
- package/dist/analyzers/KeywordExtractor.js +253 -0
- package/dist/analyzers/KeywordExtractor.js.map +1 -0
- package/dist/analyzers/PatternDetector.d.ts +38 -0
- package/dist/analyzers/PatternDetector.js +244 -0
- package/dist/analyzers/PatternDetector.js.map +1 -0
- package/dist/analyzers/SimilarityAnalyzer.d.ts +23 -0
- package/dist/analyzers/SimilarityAnalyzer.js +153 -0
- package/dist/analyzers/SimilarityAnalyzer.js.map +1 -0
- package/dist/config/defaultPrompts.d.ts +5 -0
- package/dist/config/defaultPrompts.js +22 -0
- package/dist/config/defaultPrompts.js.map +1 -0
- package/dist/core/ConversationAnalyzer.d.ts +51 -0
- package/dist/core/ConversationAnalyzer.js +213 -0
- package/dist/core/ConversationAnalyzer.js.map +1 -0
- package/dist/core/ManneriDetector.d.ts +64 -0
- package/dist/core/ManneriDetector.js +251 -0
- package/dist/core/ManneriDetector.js.map +1 -0
- package/dist/generators/PromptGenerator.d.ts +15 -0
- package/dist/generators/PromptGenerator.js +45 -0
- package/dist/generators/PromptGenerator.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/persistence/LocalStoragePersistenceProvider.d.ts +45 -0
- package/dist/persistence/LocalStoragePersistenceProvider.js +102 -0
- package/dist/persistence/LocalStoragePersistenceProvider.js.map +1 -0
- package/dist/persistence/index.d.ts +5 -0
- package/dist/persistence/index.js +5 -0
- package/dist/persistence/index.js.map +1 -0
- package/dist/types/index.d.ts +114 -0
- package/dist/types/index.js +28 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/persistence.d.ts +78 -0
- package/dist/types/persistence.js +2 -0
- package/dist/types/persistence.js.map +1 -0
- package/dist/types/prompts.d.ts +22 -0
- package/dist/types/prompts.js +36 -0
- package/dist/types/prompts.js.map +1 -0
- package/dist/utils/browserUtils.d.ts +20 -0
- package/dist/utils/browserUtils.js +206 -0
- package/dist/utils/browserUtils.js.map +1 -0
- package/dist/utils/textUtils.d.ts +10 -0
- package/dist/utils/textUtils.js +269 -0
- package/dist/utils/textUtils.js.map +1 -0
- package/package.json +50 -0
package/README.md
ADDED
|
@@ -0,0 +1,600 @@
|
|
|
1
|
+
# @aituber-onair/manneri
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
**Manneri** is a simple JavaScript library designed to detect repetitive conversation patterns in AI chatbots and provide topic diversification prompts for more engaging conversations.
|
|
6
|
+
|
|
7
|
+
[日本語版README](README.ja.md) | [English README](README.md)
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- 🔍 **Conversation Similarity Analysis**: Calculate text similarity to detect repetitive patterns
|
|
12
|
+
- 📊 **Pattern Detection**: Identify structural patterns in conversations
|
|
13
|
+
- 🎯 **Keyword Analysis**: Detect overused vocabulary and topic bias
|
|
14
|
+
- 💡 **Automatic Prompt Generation**: Generate appropriate prompts for topic diversification
|
|
15
|
+
- 🌐 **Frontend-Only**: Lightweight operation optimized for browser environments
|
|
16
|
+
- 🌍 **Multi-Language Support**: Built-in support for Japanese and English, with easy extension to any language via custom prompts
|
|
17
|
+
- 🎨 **Customizable Prompts**: Configure intervention messages and recommendations for any language
|
|
18
|
+
- 🇯🇵 **Japanese Language Support**: Proper handling of Japanese text (Hiragana, Katakana, Kanji)
|
|
19
|
+
- 💾 **Flexible Persistence**: Configurable data persistence with support for multiple storage backends
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @aituber-onair/manneri
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Basic Usage
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { ManneriDetector, LocalStoragePersistenceProvider } from '@aituber-onair/manneri';
|
|
31
|
+
|
|
32
|
+
// Create ManneriDetector with default settings
|
|
33
|
+
const detector = new ManneriDetector();
|
|
34
|
+
|
|
35
|
+
// Define message array
|
|
36
|
+
const messages = [
|
|
37
|
+
{ role: 'user', content: 'Hello' },
|
|
38
|
+
{ role: 'assistant', content: 'Hello! How can I help you today?' },
|
|
39
|
+
{ role: 'user', content: 'Hello' },
|
|
40
|
+
{ role: 'assistant', content: 'Hello! What can I assist you with?' }
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
// Detect repetitive patterns
|
|
44
|
+
if (detector.detectManneri(messages)) {
|
|
45
|
+
console.log('Repetitive conversation detected');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Check if intervention is needed (considering cooldown)
|
|
49
|
+
if (detector.shouldIntervene(messages)) {
|
|
50
|
+
// Generate topic diversification prompt
|
|
51
|
+
const prompt = detector.generateDiversificationPrompt(messages);
|
|
52
|
+
console.log('Suggested prompt:', prompt.content);
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Configuration Options
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
const detector = new ManneriDetector({
|
|
60
|
+
similarityThreshold: 0.75, // Similarity threshold (0-1)
|
|
61
|
+
repetitionLimit: 3, // Number of repetitions to detect
|
|
62
|
+
lookbackWindow: 10, // Number of messages to analyze
|
|
63
|
+
interventionCooldown: 300000, // Intervention interval (milliseconds)
|
|
64
|
+
minMessageLength: 10, // Minimum character count for analysis
|
|
65
|
+
excludeKeywords: ['yes', 'no'], // Keywords to exclude
|
|
66
|
+
enableTopicTracking: true, // Enable topic tracking
|
|
67
|
+
enableKeywordAnalysis: true, // Enable keyword analysis
|
|
68
|
+
debugMode: false, // Debug mode
|
|
69
|
+
language: 'en', // Language for prompts ('ja' | 'en' | custom)
|
|
70
|
+
customPrompts: { // Custom intervention prompts (optional)
|
|
71
|
+
en: {
|
|
72
|
+
intervention: [
|
|
73
|
+
'Please change the topic and talk about something new.',
|
|
74
|
+
'Let\'s explore a different subject.',
|
|
75
|
+
'How about discussing something else?'
|
|
76
|
+
]
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}, {
|
|
80
|
+
// Optional: Configure persistence provider
|
|
81
|
+
persistenceProvider: new LocalStoragePersistenceProvider({
|
|
82
|
+
storageKey: 'my_manneri_data',
|
|
83
|
+
version: '1.0.0'
|
|
84
|
+
})
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Integration with AITuberOnAirCore
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import { ManneriDetector } from '@aituber-onair/manneri';
|
|
92
|
+
|
|
93
|
+
const manneriDetector = new ManneriDetector({
|
|
94
|
+
similarityThreshold: 0.8,
|
|
95
|
+
repetitionLimit: 3,
|
|
96
|
+
interventionCooldown: 300000
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Integration via AITuberOnAirCore event listener
|
|
100
|
+
core.on('beforeAIRequest', (requestData) => {
|
|
101
|
+
const chatHistory = core.getChatHistory();
|
|
102
|
+
|
|
103
|
+
if (manneriDetector.shouldIntervene(chatHistory)) {
|
|
104
|
+
const diversificationPrompt = manneriDetector.generateDiversificationPrompt(chatHistory);
|
|
105
|
+
|
|
106
|
+
// Add topic change instruction as system prompt
|
|
107
|
+
requestData.messages.unshift({
|
|
108
|
+
role: 'system',
|
|
109
|
+
content: diversificationPrompt.content
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
console.log('Applied diversification prompt:', diversificationPrompt.type);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Event Handling
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
// Similarity calculation event
|
|
121
|
+
detector.on('similarity_calculated', (data) => {
|
|
122
|
+
console.log(`Similarity: ${data.score}, Threshold: ${data.threshold}`);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Pattern detection event
|
|
126
|
+
detector.on('pattern_detected', (result) => {
|
|
127
|
+
console.log('Detected patterns:', result.patterns);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// Intervention triggered event
|
|
131
|
+
detector.on('intervention_triggered', (prompt) => {
|
|
132
|
+
console.log('Intervention executed:', prompt.content);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
// Configuration update event
|
|
136
|
+
detector.on('config_updated', (newConfig) => {
|
|
137
|
+
console.log('Configuration updated:', newConfig);
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Detailed Analysis
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
// Perform detailed conversation analysis
|
|
145
|
+
const analysis = detector.analyzeConversation(messages);
|
|
146
|
+
|
|
147
|
+
console.log('Analysis result:', {
|
|
148
|
+
similarity: analysis.similarity,
|
|
149
|
+
topics: analysis.topics,
|
|
150
|
+
patterns: analysis.patterns,
|
|
151
|
+
shouldIntervene: analysis.shouldIntervene,
|
|
152
|
+
reason: analysis.interventionReason
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Get statistics
|
|
156
|
+
const stats = detector.getStatistics();
|
|
157
|
+
console.log('Statistics:', {
|
|
158
|
+
totalInterventions: stats.totalInterventions,
|
|
159
|
+
averageInterval: stats.averageInterventionInterval,
|
|
160
|
+
thresholds: stats.configuredThresholds
|
|
161
|
+
});
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Multi-Language Support
|
|
165
|
+
|
|
166
|
+
### Language Configuration
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
// Use English prompts
|
|
170
|
+
const detectorEn = new ManneriDetector({
|
|
171
|
+
language: 'en'
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
// Use Japanese prompts (default)
|
|
175
|
+
const detectorJa = new ManneriDetector({
|
|
176
|
+
language: 'ja'
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Custom prompts for Spanish
|
|
180
|
+
const detectorSpanish = new ManneriDetector({
|
|
181
|
+
language: 'es',
|
|
182
|
+
customPrompts: {
|
|
183
|
+
es: {
|
|
184
|
+
intervention: [
|
|
185
|
+
'Cambiemos de tema y hablemos de algo nuevo.',
|
|
186
|
+
'Exploremos un tema diferente.',
|
|
187
|
+
'¿Qué tal si discutimos otra cosa?'
|
|
188
|
+
]
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// Chinese support
|
|
194
|
+
const detectorChinese = new ManneriDetector({
|
|
195
|
+
language: 'zh',
|
|
196
|
+
customPrompts: {
|
|
197
|
+
zh: {
|
|
198
|
+
intervention: [
|
|
199
|
+
'让我们换个话题,聊些新的内容。',
|
|
200
|
+
'我们来探讨一个不同的主题。',
|
|
201
|
+
'要不要讨论别的事情?'
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
});
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Built-in Language Support
|
|
209
|
+
|
|
210
|
+
Manneri comes with built-in prompts for:
|
|
211
|
+
- **Japanese** (`'ja'`) - Default language
|
|
212
|
+
- **English** (`'en'`) - Full prompt coverage
|
|
213
|
+
|
|
214
|
+
```typescript
|
|
215
|
+
import { DEFAULT_PROMPTS } from '@aituber-onair/manneri';
|
|
216
|
+
|
|
217
|
+
// View available built-in languages
|
|
218
|
+
console.log(Object.keys(DEFAULT_PROMPTS)); // ['ja', 'en']
|
|
219
|
+
|
|
220
|
+
// Access specific language prompts
|
|
221
|
+
const englishPrompts = DEFAULT_PROMPTS.en;
|
|
222
|
+
console.log(englishPrompts.intervention);
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Adding Any Language
|
|
226
|
+
|
|
227
|
+
**Any language can be supported** by providing custom prompts. The library accepts any language code and uses your custom prompts:
|
|
228
|
+
|
|
229
|
+
```typescript
|
|
230
|
+
// Add support for any language (French example)
|
|
231
|
+
const detectorFrench = new ManneriDetector({
|
|
232
|
+
language: 'fr',
|
|
233
|
+
customPrompts: {
|
|
234
|
+
fr: {
|
|
235
|
+
intervention: [
|
|
236
|
+
'Changeons de sujet et parlons de quelque chose de nouveau.',
|
|
237
|
+
'Explorons un sujet différent.',
|
|
238
|
+
'Que diriez-vous de discuter d\'autre chose?'
|
|
239
|
+
]
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Or extend existing languages
|
|
245
|
+
import { overridePrompts, DEFAULT_PROMPTS } from '@aituber-onair/manneri';
|
|
246
|
+
|
|
247
|
+
const multilingualPrompts = overridePrompts(DEFAULT_PROMPTS, {
|
|
248
|
+
zh: {
|
|
249
|
+
intervention: ['让我们换个话题,聊些新的内容。']
|
|
250
|
+
},
|
|
251
|
+
ko: {
|
|
252
|
+
intervention: ['새로운 주제로 변경해 주세요.']
|
|
253
|
+
},
|
|
254
|
+
fr: {
|
|
255
|
+
intervention: ['Changeons de sujet.']
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
### Language-Agnostic Design
|
|
261
|
+
|
|
262
|
+
The library is designed to work with **any language** without code changes:
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
// Easy language switching
|
|
266
|
+
const createDetectorForLanguage = (lang: string, prompts: any) => {
|
|
267
|
+
return new ManneriDetector({
|
|
268
|
+
language: lang,
|
|
269
|
+
customPrompts: { [lang]: prompts }
|
|
270
|
+
});
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
// Support multiple languages in the same application
|
|
274
|
+
const detectors = {
|
|
275
|
+
english: createDetectorForLanguage('en', englishPrompts),
|
|
276
|
+
chinese: createDetectorForLanguage('zh', chinesePrompts),
|
|
277
|
+
korean: createDetectorForLanguage('ko', koreanPrompts),
|
|
278
|
+
arabic: createDetectorForLanguage('ar', arabicPrompts),
|
|
279
|
+
// Add any language
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
// Dynamic language detection
|
|
283
|
+
const getUserLanguage = () => navigator.language.split('-')[0];
|
|
284
|
+
const userDetector = detectors[getUserLanguage()] || detectors.english;
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
### Prompt Utilities
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
import { getPromptTemplate, overridePrompts } from '@aituber-onair/manneri';
|
|
291
|
+
|
|
292
|
+
// Get intervention prompt for any language
|
|
293
|
+
const interventionPrompt = getPromptTemplate(
|
|
294
|
+
myCustomPrompts,
|
|
295
|
+
'zh'
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
// Override default prompts with custom ones
|
|
299
|
+
const globalPrompts = overridePrompts(DEFAULT_PROMPTS, {
|
|
300
|
+
zh: { intervention: ['换个话题吧'] },
|
|
301
|
+
ko: { intervention: ['주제를 바꿔주세요'] },
|
|
302
|
+
es: { intervention: ['Cambiemos de tema'] }
|
|
303
|
+
});
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
## Individual Feature Usage
|
|
307
|
+
|
|
308
|
+
### Similarity Analysis
|
|
309
|
+
|
|
310
|
+
```typescript
|
|
311
|
+
import { SimilarityAnalyzer } from '@aituber-onair/manneri';
|
|
312
|
+
|
|
313
|
+
const analyzer = new SimilarityAnalyzer();
|
|
314
|
+
const similarity = analyzer.calculateSimilarity('Hello', 'Hello, how are you?');
|
|
315
|
+
console.log('Similarity:', similarity); // 0.0 - 1.0
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Keyword Extraction
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
import { KeywordExtractor } from '@aituber-onair/manneri';
|
|
322
|
+
|
|
323
|
+
const extractor = new KeywordExtractor();
|
|
324
|
+
const keywords = extractor.extractKeywordsFromMessages(messages);
|
|
325
|
+
console.log('Keywords:', keywords);
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
### Pattern Detection
|
|
329
|
+
|
|
330
|
+
```typescript
|
|
331
|
+
import { PatternDetector } from '@aituber-onair/manneri';
|
|
332
|
+
|
|
333
|
+
const detector = new PatternDetector();
|
|
334
|
+
const result = detector.detectPatterns(messages);
|
|
335
|
+
console.log('Patterns:', result.patterns);
|
|
336
|
+
console.log('Severity:', result.severity);
|
|
337
|
+
console.log('Confidence:', result.confidence);
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
## Data Persistence
|
|
341
|
+
|
|
342
|
+
Manneri provides flexible persistence through configurable providers. You have full control over when and how data is saved.
|
|
343
|
+
|
|
344
|
+
### Browser Environment (LocalStorage)
|
|
345
|
+
|
|
346
|
+
```typescript
|
|
347
|
+
import { ManneriDetector, LocalStoragePersistenceProvider } from '@aituber-onair/manneri';
|
|
348
|
+
|
|
349
|
+
// Configure with LocalStorage persistence
|
|
350
|
+
const detector = new ManneriDetector({
|
|
351
|
+
// ... configuration options
|
|
352
|
+
}, {
|
|
353
|
+
persistenceProvider: new LocalStoragePersistenceProvider({
|
|
354
|
+
storageKey: 'manneri_data', // Custom storage key
|
|
355
|
+
version: '1.0.0' // Data version
|
|
356
|
+
})
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Manual persistence control
|
|
360
|
+
await detector.save(); // Save current state
|
|
361
|
+
await detector.load(); // Load saved state
|
|
362
|
+
await detector.cleanup(); // Clean up old data
|
|
363
|
+
|
|
364
|
+
// Check if persistence is available
|
|
365
|
+
if (detector.hasPersistenceProvider()) {
|
|
366
|
+
console.log('Persistence is configured');
|
|
367
|
+
|
|
368
|
+
// Get storage info
|
|
369
|
+
const info = detector.getPersistenceInfo();
|
|
370
|
+
console.log('Storage info:', info);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Event handling for persistence operations
|
|
374
|
+
detector.on('save_success', ({ timestamp }) => {
|
|
375
|
+
console.log('Data saved successfully at', new Date(timestamp));
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
detector.on('save_error', ({ error }) => {
|
|
379
|
+
console.error('Failed to save data:', error);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
detector.on('load_success', ({ data, timestamp }) => {
|
|
383
|
+
console.log('Data loaded successfully:', data);
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
detector.on('cleanup_completed', ({ removedItems, timestamp }) => {
|
|
387
|
+
console.log(`Cleaned up ${removedItems} old items`);
|
|
388
|
+
});
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### Custom Persistence Provider
|
|
392
|
+
|
|
393
|
+
For Node.js, Deno, or custom storage solutions, implement the `PersistenceProvider` interface:
|
|
394
|
+
|
|
395
|
+
```typescript
|
|
396
|
+
import type { PersistenceProvider, StorageData } from '@aituber-onair/manneri';
|
|
397
|
+
|
|
398
|
+
// Example: Database persistence provider
|
|
399
|
+
class DatabasePersistenceProvider implements PersistenceProvider {
|
|
400
|
+
constructor(private dbConnection: any) {}
|
|
401
|
+
|
|
402
|
+
async save(data: StorageData): Promise<boolean> {
|
|
403
|
+
try {
|
|
404
|
+
await this.dbConnection.query(
|
|
405
|
+
'INSERT OR REPLACE INTO manneri_data (id, data) VALUES (?, ?)',
|
|
406
|
+
[1, JSON.stringify(data)]
|
|
407
|
+
);
|
|
408
|
+
return true;
|
|
409
|
+
} catch (error) {
|
|
410
|
+
console.error('Database save failed:', error);
|
|
411
|
+
return false;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async load(): Promise<StorageData | null> {
|
|
416
|
+
try {
|
|
417
|
+
const result = await this.dbConnection.query(
|
|
418
|
+
'SELECT data FROM manneri_data WHERE id = ?',
|
|
419
|
+
[1]
|
|
420
|
+
);
|
|
421
|
+
return result.length > 0 ? JSON.parse(result[0].data) : null;
|
|
422
|
+
} catch (error) {
|
|
423
|
+
console.error('Database load failed:', error);
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async clear(): Promise<boolean> {
|
|
429
|
+
try {
|
|
430
|
+
await this.dbConnection.query('DELETE FROM manneri_data WHERE id = ?', [1]);
|
|
431
|
+
return true;
|
|
432
|
+
} catch (error) {
|
|
433
|
+
console.error('Database clear failed:', error);
|
|
434
|
+
return false;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async cleanup(maxAge: number): Promise<number> {
|
|
439
|
+
// Implement cleanup logic for your storage
|
|
440
|
+
// Return number of items removed
|
|
441
|
+
return 0;
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Use custom persistence provider
|
|
446
|
+
const detector = new ManneriDetector({
|
|
447
|
+
// ... configuration
|
|
448
|
+
}, {
|
|
449
|
+
persistenceProvider: new DatabasePersistenceProvider(dbConnection)
|
|
450
|
+
});
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
### Manual Data Management (No Persistence)
|
|
454
|
+
|
|
455
|
+
```typescript
|
|
456
|
+
// Use without persistence provider
|
|
457
|
+
const detector = new ManneriDetector();
|
|
458
|
+
|
|
459
|
+
// Manual export/import for custom storage
|
|
460
|
+
const data = detector.exportData();
|
|
461
|
+
// Store data however you want (file, database, etc.)
|
|
462
|
+
await myCustomStorage.save(data);
|
|
463
|
+
|
|
464
|
+
// Restore data
|
|
465
|
+
const restoredData = await myCustomStorage.load();
|
|
466
|
+
detector.importData(restoredData);
|
|
467
|
+
|
|
468
|
+
// Clear history
|
|
469
|
+
detector.clearHistory();
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
### Environment-Specific Examples
|
|
473
|
+
|
|
474
|
+
```typescript
|
|
475
|
+
// Browser-only with LocalStorage
|
|
476
|
+
const browserDetector = new ManneriDetector({}, {
|
|
477
|
+
persistenceProvider: new LocalStoragePersistenceProvider()
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
// Node.js with file storage
|
|
481
|
+
class FilePersistenceProvider implements PersistenceProvider {
|
|
482
|
+
constructor(private filePath: string) {}
|
|
483
|
+
|
|
484
|
+
save(data: StorageData): boolean {
|
|
485
|
+
try {
|
|
486
|
+
fs.writeFileSync(this.filePath, JSON.stringify(data));
|
|
487
|
+
return true;
|
|
488
|
+
} catch {
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
load(): StorageData | null {
|
|
494
|
+
try {
|
|
495
|
+
const data = fs.readFileSync(this.filePath, 'utf8');
|
|
496
|
+
return JSON.parse(data);
|
|
497
|
+
} catch {
|
|
498
|
+
return null;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
clear(): boolean {
|
|
503
|
+
try {
|
|
504
|
+
fs.unlinkSync(this.filePath);
|
|
505
|
+
return true;
|
|
506
|
+
} catch {
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const nodeDetector = new ManneriDetector({}, {
|
|
513
|
+
persistenceProvider: new FilePersistenceProvider('./manneri-data.json')
|
|
514
|
+
});
|
|
515
|
+
|
|
516
|
+
// Deno with Deno.KV
|
|
517
|
+
class DenoKvPersistenceProvider implements PersistenceProvider {
|
|
518
|
+
constructor(private kv: Deno.Kv) {}
|
|
519
|
+
|
|
520
|
+
async save(data: StorageData): Promise<boolean> {
|
|
521
|
+
try {
|
|
522
|
+
await this.kv.set(['manneri', 'data'], data);
|
|
523
|
+
return true;
|
|
524
|
+
} catch {
|
|
525
|
+
return false;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
async load(): Promise<StorageData | null> {
|
|
530
|
+
try {
|
|
531
|
+
const result = await this.kv.get(['manneri', 'data']);
|
|
532
|
+
return result.value as StorageData | null;
|
|
533
|
+
} catch {
|
|
534
|
+
return null;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
async clear(): Promise<boolean> {
|
|
539
|
+
try {
|
|
540
|
+
await this.kv.delete(['manneri', 'data']);
|
|
541
|
+
return true;
|
|
542
|
+
} catch {
|
|
543
|
+
return false;
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const kv = await Deno.openKv();
|
|
549
|
+
const denoDetector = new ManneriDetector({}, {
|
|
550
|
+
persistenceProvider: new DenoKvPersistenceProvider(kv)
|
|
551
|
+
});
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
## TypeScript Type Definitions
|
|
555
|
+
|
|
556
|
+
```typescript
|
|
557
|
+
import type {
|
|
558
|
+
Message,
|
|
559
|
+
ManneriConfig,
|
|
560
|
+
AnalysisResult,
|
|
561
|
+
DiversificationPrompt,
|
|
562
|
+
LocalizedPrompts,
|
|
563
|
+
PromptTemplates,
|
|
564
|
+
SupportedLanguage,
|
|
565
|
+
PersistenceProvider,
|
|
566
|
+
PersistenceConfig,
|
|
567
|
+
StorageData
|
|
568
|
+
} from '@aituber-onair/manneri';
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
## Browser Support
|
|
572
|
+
|
|
573
|
+
- Chrome 90+
|
|
574
|
+
- Firefox 88+
|
|
575
|
+
- Safari 14+
|
|
576
|
+
- Edge 90+
|
|
577
|
+
|
|
578
|
+
## Performance
|
|
579
|
+
|
|
580
|
+
- Lightweight: < 50KB gzipped
|
|
581
|
+
- Fast: Real-time analysis < 100ms
|
|
582
|
+
- Memory efficient: Automatic cache cleanup
|
|
583
|
+
|
|
584
|
+
## License
|
|
585
|
+
|
|
586
|
+
MIT License
|
|
587
|
+
|
|
588
|
+
## Contributing
|
|
589
|
+
|
|
590
|
+
Pull requests and issues are welcome. Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.
|
|
591
|
+
|
|
592
|
+
## Support
|
|
593
|
+
|
|
594
|
+
- GitHub Issues: https://github.com/shinshin86/aituber-onair/issues
|
|
595
|
+
- Documentation: https://github.com/shinshin86/aituber-onair/tree/main/packages/manneri
|
|
596
|
+
|
|
597
|
+
## Related Projects
|
|
598
|
+
|
|
599
|
+
- [AITuber OnAir](https://github.com/shinshin86/aituber-onair) - Main project
|
|
600
|
+
- [@aituber-onair/core](https://github.com/shinshin86/aituber-onair/tree/main/packages/core) - Core library
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Message, TopicInfo, TextAnalysisOptions } from '../types/index.js';
|
|
2
|
+
export interface KeywordFrequency {
|
|
3
|
+
keyword: string;
|
|
4
|
+
frequency: number;
|
|
5
|
+
score: number;
|
|
6
|
+
firstSeen: number;
|
|
7
|
+
lastSeen: number;
|
|
8
|
+
contexts: string[];
|
|
9
|
+
}
|
|
10
|
+
export interface TopicCluster {
|
|
11
|
+
id: string;
|
|
12
|
+
keywords: string[];
|
|
13
|
+
score: number;
|
|
14
|
+
messageCount: number;
|
|
15
|
+
firstMessage: number;
|
|
16
|
+
lastMessage: number;
|
|
17
|
+
}
|
|
18
|
+
export declare class KeywordExtractor {
|
|
19
|
+
private readonly options;
|
|
20
|
+
private readonly keywordFrequencies;
|
|
21
|
+
private readonly topicClusters;
|
|
22
|
+
private readonly maxKeywords;
|
|
23
|
+
private readonly maxContextLength;
|
|
24
|
+
constructor(options?: Partial<TextAnalysisOptions>);
|
|
25
|
+
extractKeywordsFromMessage(message: Message): string[];
|
|
26
|
+
extractKeywordsFromMessages(messages: Message[]): string[];
|
|
27
|
+
analyzeKeywordFrequencies(messages: Message[]): KeywordFrequency[];
|
|
28
|
+
detectTopicShift(recentMessages: Message[], historicalMessages: Message[], threshold?: number): {
|
|
29
|
+
hasShift: boolean;
|
|
30
|
+
newTopics: string[];
|
|
31
|
+
oldTopics: string[];
|
|
32
|
+
};
|
|
33
|
+
analyzeTopicClusters(messages: Message[]): TopicCluster[];
|
|
34
|
+
getTopicInfo(messages: Message[]): TopicInfo[];
|
|
35
|
+
findRepeatedKeywords(messages: Message[], minRepetitions?: number, windowSize?: number): Array<{
|
|
36
|
+
keyword: string;
|
|
37
|
+
positions: number[];
|
|
38
|
+
density: number;
|
|
39
|
+
}>;
|
|
40
|
+
private calculateKeywordScore;
|
|
41
|
+
private findRelatedKeywords;
|
|
42
|
+
private calculateSemanticSimilarity;
|
|
43
|
+
private generateClusterId;
|
|
44
|
+
private calculateClusterScore;
|
|
45
|
+
private categorizeKeywords;
|
|
46
|
+
private calculateKeywordDensity;
|
|
47
|
+
clearCache(): void;
|
|
48
|
+
}
|