make-mp-data 2.0.22 → 2.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/dungeons/ai-chat-analytics-ed.js +274 -0
- package/dungeons/business.js +0 -1
- package/dungeons/complex.js +0 -1
- package/dungeons/experiments.js +0 -1
- package/dungeons/gaming.js +47 -14
- package/dungeons/media.js +5 -6
- package/dungeons/mil.js +296 -0
- package/dungeons/money2020-ed-also.js +277 -0
- package/dungeons/money2020-ed.js +579 -0
- package/dungeons/sanity.js +0 -1
- package/dungeons/scd.js +0 -1
- package/dungeons/simple.js +57 -18
- package/dungeons/student-teacher.js +0 -1
- package/dungeons/text-generation.js +706 -0
- package/dungeons/userAgent.js +1 -2
- package/entry.js +4 -0
- package/index.js +63 -38
- package/lib/cli/cli.js +7 -8
- package/lib/core/config-validator.js +11 -13
- package/lib/core/context.js +13 -1
- package/lib/core/storage.js +45 -13
- package/lib/generators/adspend.js +1 -1
- package/lib/generators/events.js +18 -17
- package/lib/generators/funnels.js +293 -240
- package/lib/generators/text-bak-old.js +1121 -0
- package/lib/generators/text.js +1173 -0
- package/lib/orchestrators/mixpanel-sender.js +1 -1
- package/lib/templates/abbreviated.d.ts +13 -3
- package/lib/templates/defaults.js +311 -169
- package/lib/templates/hooks-instructions.txt +434 -0
- package/lib/templates/phrases-bak.js +925 -0
- package/lib/templates/phrases.js +2066 -0
- package/lib/templates/{instructions.txt → schema-instructions.txt} +78 -1
- package/lib/templates/scratch-dungeon-template.js +1 -1
- package/lib/templates/textQuickTest.js +172 -0
- package/lib/utils/ai.js +51 -2
- package/lib/utils/utils.js +145 -7
- package/package.json +8 -5
- package/types.d.ts +322 -7
- package/lib/utils/chart.js +0 -206
|
@@ -0,0 +1,1173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Organic Text Generation Module
|
|
3
|
+
* Generates genuinely human-feeling unstructured text
|
|
4
|
+
* @module text
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// ============= Type Imports =============
|
|
8
|
+
// Types are defined in types.d.ts
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {import("../../types").TextTone} TextTone
|
|
12
|
+
* @typedef {import("../../types").TextStyle} TextStyle
|
|
13
|
+
* @typedef {import("../../types").TextIntensity} TextIntensity
|
|
14
|
+
* @typedef {import("../../types").TextFormality} TextFormality
|
|
15
|
+
* @typedef {import("../../types").TextReturnType} TextReturnType
|
|
16
|
+
* @typedef {import("../../types").TextKeywordSet} TextKeywordSet
|
|
17
|
+
* @typedef {import("../../types").TextGeneratorConfig} TextGeneratorConfig
|
|
18
|
+
* @typedef {import("../../types").TextMetadata} TextMetadata
|
|
19
|
+
* @typedef {import("../../types").GeneratedText} GeneratedText
|
|
20
|
+
* @typedef {import("../../types").SimpleGeneratedText} SimpleGeneratedText
|
|
21
|
+
* @typedef {import("../../types").TextBatchOptions} TextBatchOptions
|
|
22
|
+
* @typedef {import("../../types").TextGeneratorStats} TextGeneratorStats
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import tracery from 'tracery-grammar';
|
|
26
|
+
import seedrandom from 'seedrandom';
|
|
27
|
+
import crypto from 'crypto';
|
|
28
|
+
import SentimentPkg from 'sentiment';
|
|
29
|
+
import { PHRASE_BANK, GENERATION_PATTERNS, ORGANIC_PATTERNS } from '../templates/phrases.js';
|
|
30
|
+
|
|
31
|
+
const Sentiment = typeof SentimentPkg === 'function' ? SentimentPkg : SentimentPkg.default;
|
|
32
|
+
const sentiment = new Sentiment();
|
|
33
|
+
|
|
34
|
+
// ============= Helper Functions =============
|
|
35
|
+
|
|
36
|
+
function chance(probability) {
|
|
37
|
+
return Math.random() < probability;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function pick(array) {
|
|
41
|
+
return array[Math.floor(Math.random() * array.length)];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function pickWeighted(items, weights) {
|
|
45
|
+
const total = weights.reduce((a, b) => a + b, 0);
|
|
46
|
+
let random = Math.random() * total;
|
|
47
|
+
for (let i = 0; i < items.length; i++) {
|
|
48
|
+
random -= weights[i];
|
|
49
|
+
if (random <= 0) return items[i];
|
|
50
|
+
}
|
|
51
|
+
return items[0];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ============= Thought Stream Generator =============
|
|
55
|
+
|
|
56
|
+
class ThoughtStream {
|
|
57
|
+
constructor() {
|
|
58
|
+
this.momentum = {
|
|
59
|
+
emotional: 0,
|
|
60
|
+
technical: 0,
|
|
61
|
+
frustration: 0,
|
|
62
|
+
excitement: 0
|
|
63
|
+
};
|
|
64
|
+
this.lastTopic = null;
|
|
65
|
+
this.thoughtHistory = [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
reset() {
|
|
69
|
+
this.momentum = { emotional: 0, technical: 0, frustration: 0, excitement: 0 };
|
|
70
|
+
this.lastTopic = null;
|
|
71
|
+
this.thoughtHistory = [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
generateThought(tone, context = {}) {
|
|
75
|
+
const patterns = ORGANIC_PATTERNS.thoughtPatterns[tone] || ORGANIC_PATTERNS.thoughtPatterns.neu;
|
|
76
|
+
const pattern = pick(patterns);
|
|
77
|
+
|
|
78
|
+
// Replace placeholders with context-aware content
|
|
79
|
+
let thought = this.fillPattern(pattern, context);
|
|
80
|
+
|
|
81
|
+
// Add natural variations
|
|
82
|
+
if (this.momentum.frustration > 0.5 && chance(0.3)) {
|
|
83
|
+
thought = thought.toUpperCase();
|
|
84
|
+
} else if (this.momentum.emotional > 0.7 && chance(0.4)) {
|
|
85
|
+
thought = this.addEmphasis(thought);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Update momentum
|
|
89
|
+
this.updateMomentum(thought, tone);
|
|
90
|
+
|
|
91
|
+
return thought;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
fillPattern(pattern, context) {
|
|
95
|
+
// Simple template filling - in production this would be more sophisticated
|
|
96
|
+
return pattern
|
|
97
|
+
.replace(/{product}/g, () => pick(PHRASE_BANK.products))
|
|
98
|
+
.replace(/{feature}/g, () => pick(PHRASE_BANK.features))
|
|
99
|
+
.replace(/{issue}/g, () => pick(PHRASE_BANK.issues))
|
|
100
|
+
.replace(/{emotion}/g, () => pick(PHRASE_BANK.emotions[context.tone || 'neu']));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
addEmphasis(text) {
|
|
104
|
+
const methods = [
|
|
105
|
+
t => t.replace(/\s+/g, '. ').toUpperCase() + '.',
|
|
106
|
+
t => t.split(' ').map(w => w.length > 3 && chance(0.3) ? w.toUpperCase() : w).join(' '),
|
|
107
|
+
t => t + '!!!',
|
|
108
|
+
t => '...' + t + '...',
|
|
109
|
+
t => t.split(' ').join(' 👏 ')
|
|
110
|
+
];
|
|
111
|
+
return pick(methods)(text);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
updateMomentum(thought, tone) {
|
|
115
|
+
// Emotional momentum
|
|
116
|
+
if (tone === 'neg') {
|
|
117
|
+
this.momentum.frustration = Math.min(1, this.momentum.frustration + 0.2);
|
|
118
|
+
this.momentum.excitement *= 0.8;
|
|
119
|
+
} else if (tone === 'pos') {
|
|
120
|
+
this.momentum.excitement = Math.min(1, this.momentum.excitement + 0.2);
|
|
121
|
+
this.momentum.frustration *= 0.8;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Technical momentum
|
|
125
|
+
if (/\b(API|backend|database|server|deployment)\b/i.test(thought)) {
|
|
126
|
+
this.momentum.technical = Math.min(1, this.momentum.technical + 0.3);
|
|
127
|
+
} else {
|
|
128
|
+
this.momentum.technical *= 0.9;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// General emotional buildup
|
|
132
|
+
if (/[!?]{2,}|[A-Z]{5,}/.test(thought)) {
|
|
133
|
+
this.momentum.emotional = Math.min(1, this.momentum.emotional + 0.3);
|
|
134
|
+
} else {
|
|
135
|
+
this.momentum.emotional *= 0.95;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
this.thoughtHistory.push(thought);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
connect(thoughts) {
|
|
142
|
+
if (thoughts.length === 0) return '';
|
|
143
|
+
if (thoughts.length === 1) return thoughts[0];
|
|
144
|
+
|
|
145
|
+
const connected = [];
|
|
146
|
+
for (let i = 0; i < thoughts.length; i++) {
|
|
147
|
+
connected.push(thoughts[i]);
|
|
148
|
+
|
|
149
|
+
if (i < thoughts.length - 1) {
|
|
150
|
+
// Choose connector based on momentum
|
|
151
|
+
if (this.momentum.frustration > 0.6) {
|
|
152
|
+
connected.push(pick(['. AND ', '. ALSO ', '. Plus ', '. Oh and ', '... ']));
|
|
153
|
+
} else if (this.momentum.excitement > 0.6) {
|
|
154
|
+
connected.push(pick(['! And ', '! Also ', '!! ', '! Oh and ', '! ']));
|
|
155
|
+
} else {
|
|
156
|
+
connected.push(pick(['. ', ', ', '... ', ' - ', '. So ', '. But ', '. Well, ', '. Now, ', '. Then, ', '. Still, ']));
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return connected.join('');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ============= Context Tracker =============
|
|
166
|
+
|
|
167
|
+
class ContextTracker {
|
|
168
|
+
constructor() {
|
|
169
|
+
this.reset();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
reset() {
|
|
173
|
+
this.topics = [];
|
|
174
|
+
this.sentimentHistory = [];
|
|
175
|
+
this.currentVoice = null;
|
|
176
|
+
this.technicalLevel = 0;
|
|
177
|
+
this.formalityLevel = 0;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
update(text) {
|
|
181
|
+
// Extract topics
|
|
182
|
+
const topicMatches = text.match(/\b(dashboard|feature|API|app|system|tool|platform)\b/gi);
|
|
183
|
+
if (topicMatches) {
|
|
184
|
+
this.topics.push(...topicMatches);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Track sentiment
|
|
188
|
+
const score = sentiment.analyze(text).score;
|
|
189
|
+
this.sentimentHistory.push(score);
|
|
190
|
+
|
|
191
|
+
// Detect voice
|
|
192
|
+
if (/\b(gonna|wanna|kinda|y'all)\b/i.test(text)) {
|
|
193
|
+
this.currentVoice = 'casual';
|
|
194
|
+
} else if (/\b(furthermore|therefore|consequently)\b/i.test(text)) {
|
|
195
|
+
this.currentVoice = 'formal';
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Technical level
|
|
199
|
+
const techTerms = (text.match(/\b(API|JSON|SQL|HTTP|CSS|HTML|JavaScript|Python)\b/gi) || []).length;
|
|
200
|
+
this.technicalLevel = Math.min(1, this.technicalLevel * 0.8 + techTerms * 0.1);
|
|
201
|
+
|
|
202
|
+
// Formality level
|
|
203
|
+
const formalTerms = (text.match(/\b(regarding|concerning|therefore|furthermore)\b/gi) || []).length;
|
|
204
|
+
const casualTerms = (text.match(/\b(like|kinda|sorta|stuff|thing)\b/gi) || []).length;
|
|
205
|
+
this.formalityLevel = Math.max(0, Math.min(1, this.formalityLevel + (formalTerms - casualTerms) * 0.1));
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
getContext() {
|
|
209
|
+
return {
|
|
210
|
+
lastTopic: this.topics[this.topics.length - 1] || null,
|
|
211
|
+
averageSentiment: this.sentimentHistory.length > 0 ?
|
|
212
|
+
this.sentimentHistory.reduce((a, b) => a + b, 0) / this.sentimentHistory.length : 0,
|
|
213
|
+
voice: this.currentVoice,
|
|
214
|
+
technicalLevel: this.technicalLevel,
|
|
215
|
+
formalityLevel: this.formalityLevel
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ============= Natural Deduplicator =============
|
|
221
|
+
|
|
222
|
+
class NaturalDeduplicator {
|
|
223
|
+
constructor() {
|
|
224
|
+
this.recentTexts = [];
|
|
225
|
+
this.semanticFingerprints = new Map();
|
|
226
|
+
this.maxRecent = 100;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
wouldBeDuplicate(text) {
|
|
230
|
+
// Allow natural repetitions
|
|
231
|
+
if (this.isNaturalRepetition(text)) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Check semantic similarity
|
|
236
|
+
const fingerprint = this.getFingerprint(text);
|
|
237
|
+
const similar = this.findSimilar(fingerprint);
|
|
238
|
+
|
|
239
|
+
// Allow up to 2 very similar texts (humans repeat themselves)
|
|
240
|
+
return similar.length > 2;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
add(text) {
|
|
244
|
+
this.recentTexts.push(text);
|
|
245
|
+
if (this.recentTexts.length > this.maxRecent) {
|
|
246
|
+
this.recentTexts.shift();
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
const fingerprint = this.getFingerprint(text);
|
|
250
|
+
const key = `${fingerprint.topic}-${fingerprint.sentiment}`;
|
|
251
|
+
|
|
252
|
+
if (!this.semanticFingerprints.has(key)) {
|
|
253
|
+
this.semanticFingerprints.set(key, []);
|
|
254
|
+
}
|
|
255
|
+
this.semanticFingerprints.get(key).push(text);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
getFingerprint(text) {
|
|
259
|
+
const words = text.toLowerCase().split(/\s+/);
|
|
260
|
+
const topic = this.extractMainTopic(text);
|
|
261
|
+
|
|
262
|
+
// Use simple heuristics instead of full sentiment analysis for speed
|
|
263
|
+
const negWords = (text.match(/\b(broken|terrible|awful|bad|crash|error|fail|bug)\b/gi) || []).length;
|
|
264
|
+
const posWords = (text.match(/\b(great|excellent|amazing|good|love|perfect|works)\b/gi) || []).length;
|
|
265
|
+
const sentiment = negWords > posWords ? 'neg' : posWords > negWords ? 'pos' : 'neu';
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
topic,
|
|
269
|
+
sentiment,
|
|
270
|
+
length: words.length,
|
|
271
|
+
structure: this.getStructure(text),
|
|
272
|
+
uniqueWords: new Set(words).size
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
extractMainTopic(text) {
|
|
277
|
+
const topics = text.match(/\b(dashboard|API|feature|app|system|bug|error|issue)\b/i);
|
|
278
|
+
return topics ? topics[0].toLowerCase() : 'general';
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
getStructure(text) {
|
|
282
|
+
const sentences = text.split(/[.!?]+/).length;
|
|
283
|
+
const hasQuestion = text.includes('?');
|
|
284
|
+
const hasExclamation = text.includes('!');
|
|
285
|
+
|
|
286
|
+
return `${sentences}s${hasQuestion ? 'Q' : ''}${hasExclamation ? 'E' : ''}`;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
findSimilar(fingerprint) {
|
|
290
|
+
const key = `${fingerprint.topic}-${fingerprint.sentiment}`;
|
|
291
|
+
const candidates = this.semanticFingerprints.get(key) || [];
|
|
292
|
+
|
|
293
|
+
return candidates.filter(text => {
|
|
294
|
+
const similarity = this.calculateSimilarity(text, fingerprint);
|
|
295
|
+
return similarity > 0.7;
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
calculateSimilarity(text, targetFingerprint) {
|
|
300
|
+
const textFingerprint = this.getFingerprint(text);
|
|
301
|
+
|
|
302
|
+
let score = 0;
|
|
303
|
+
if (textFingerprint.topic === targetFingerprint.topic) score += 0.3;
|
|
304
|
+
if (textFingerprint.sentiment === targetFingerprint.sentiment) score += 0.2;
|
|
305
|
+
if (Math.abs(textFingerprint.length - targetFingerprint.length) < 10) score += 0.2;
|
|
306
|
+
if (textFingerprint.structure === targetFingerprint.structure) score += 0.3;
|
|
307
|
+
|
|
308
|
+
return score;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
isNaturalRepetition(text) {
|
|
312
|
+
// Common phrases that naturally repeat
|
|
313
|
+
const naturalPhrases = [
|
|
314
|
+
/^(hi|hey|hello|thanks|thank you)/i,
|
|
315
|
+
/^(yes|no|okay|sure|got it)/i,
|
|
316
|
+
/^(any update|following up|still waiting)/i,
|
|
317
|
+
/^(this is ridiculous|come on|seriously)/i
|
|
318
|
+
];
|
|
319
|
+
|
|
320
|
+
return naturalPhrases.some(pattern => pattern.test(text)) && text.length < 50;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ============= Voice Consistency Engine =============
|
|
325
|
+
|
|
326
|
+
class VoiceConsistency {
|
|
327
|
+
constructor(formality) {
|
|
328
|
+
this.formality = formality;
|
|
329
|
+
this.vocabulary = this.selectVocabulary(formality);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
selectVocabulary(formality) {
|
|
333
|
+
const vocabularies = {
|
|
334
|
+
casual: {
|
|
335
|
+
connectors: ['and', 'but', 'so', 'like', 'anyway'],
|
|
336
|
+
intensifiers: ['really', 'super', 'totally', 'so', 'pretty'],
|
|
337
|
+
hedges: ['kinda', 'sorta', 'I guess', 'maybe', 'probably']
|
|
338
|
+
},
|
|
339
|
+
business: {
|
|
340
|
+
connectors: ['additionally', 'however', 'therefore', 'furthermore', 'consequently'],
|
|
341
|
+
intensifiers: ['very', 'quite', 'extremely', 'particularly', 'especially'],
|
|
342
|
+
hedges: ['perhaps', 'potentially', 'possibly', 'it appears', 'it seems']
|
|
343
|
+
},
|
|
344
|
+
technical: {
|
|
345
|
+
connectors: ['additionally', 'moreover', 'specifically', 'namely', 'particularly'],
|
|
346
|
+
intensifiers: ['significantly', 'substantially', 'considerably', 'markedly'],
|
|
347
|
+
hedges: ['approximately', 'roughly', 'estimated', 'projected', 'calculated']
|
|
348
|
+
}
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
return vocabularies[formality] || vocabularies.casual;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
maintain(text) {
|
|
355
|
+
// Apply consistent voice
|
|
356
|
+
let consistent = text;
|
|
357
|
+
|
|
358
|
+
// Replace connectors
|
|
359
|
+
consistent = consistent.replace(/\b(and|but|so)\b/gi, () =>
|
|
360
|
+
pick(this.vocabulary.connectors)
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
// Apply appropriate contractions
|
|
364
|
+
if (this.formality === 'casual') {
|
|
365
|
+
consistent = consistent
|
|
366
|
+
.replace(/\bcannot\b/g, "can't")
|
|
367
|
+
.replace(/\bwill not\b/g, "won't")
|
|
368
|
+
.replace(/\bdo not\b/g, "don't");
|
|
369
|
+
} else if (this.formality === 'business' || this.formality === 'technical') {
|
|
370
|
+
consistent = consistent
|
|
371
|
+
.replace(/\bcan't\b/g, "cannot")
|
|
372
|
+
.replace(/\bwon't\b/g, "will not")
|
|
373
|
+
.replace(/\bdon't\b/g, "do not");
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return consistent;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// ============= Natural Typo Engine =============
|
|
381
|
+
|
|
382
|
+
class NaturalTypoEngine {
|
|
383
|
+
constructor() {
|
|
384
|
+
this.patterns = {
|
|
385
|
+
emotional: [
|
|
386
|
+
{ pattern: /\bthe\b/g, errors: ['teh', 'th', 'hte'] },
|
|
387
|
+
{ pattern: /\byou\b/g, errors: ['u', 'yuo'] },
|
|
388
|
+
{ pattern: /\bbecause\b/g, errors: ['becuase', 'bc', 'cuz'] },
|
|
389
|
+
{ pattern: /\bdefinitely\b/g, errors: ['definately', 'defiantly'] }
|
|
390
|
+
],
|
|
391
|
+
mobile: [
|
|
392
|
+
{ pattern: /\s+/g, errors: [''] }, // Missing spaces
|
|
393
|
+
{ pattern: /([a-z])\1/g, errors: ['$1'] } // Missing double letters
|
|
394
|
+
],
|
|
395
|
+
rushing: [
|
|
396
|
+
{ pattern: /ing\b/g, errors: ['ign', 'in'] },
|
|
397
|
+
{ pattern: /tion\b/g, errors: ['toin', 'tion'] }
|
|
398
|
+
]
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
apply(text, rate, context = {}) {
|
|
403
|
+
if (!rate || rate === 0) return text;
|
|
404
|
+
|
|
405
|
+
const words = text.split(/(\s+)/);
|
|
406
|
+
const typoClusterProbability = 0.3; // Typos tend to cluster
|
|
407
|
+
let inCluster = false;
|
|
408
|
+
|
|
409
|
+
return words.map(word => {
|
|
410
|
+
if (!/\w/.test(word)) return word; // Skip non-words
|
|
411
|
+
|
|
412
|
+
const shouldTypo = inCluster ?
|
|
413
|
+
chance(rate * 3) : // Higher chance in cluster
|
|
414
|
+
chance(rate);
|
|
415
|
+
|
|
416
|
+
if (shouldTypo) {
|
|
417
|
+
inCluster = chance(typoClusterProbability);
|
|
418
|
+
return this.createTypo(word, context);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
inCluster = false;
|
|
422
|
+
return word;
|
|
423
|
+
}).join('');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
createTypo(word, context) {
|
|
427
|
+
// Select typo type based on context
|
|
428
|
+
const patterns = context.emotional > 0.5 ? this.patterns.emotional :
|
|
429
|
+
context.mobile ? this.patterns.mobile :
|
|
430
|
+
this.patterns.rushing;
|
|
431
|
+
|
|
432
|
+
for (const { pattern, errors } of patterns) {
|
|
433
|
+
if (pattern.test(word)) {
|
|
434
|
+
return word.replace(pattern, pick(errors));
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Fallback: transpose letters
|
|
439
|
+
if (word.length > 3) {
|
|
440
|
+
const pos = Math.floor(Math.random() * (word.length - 2)) + 1;
|
|
441
|
+
return word.slice(0, pos) + word[pos + 1] + word[pos] + word.slice(pos + 2);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return word;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// ============= Keyword Injector =============
|
|
449
|
+
|
|
450
|
+
class KeywordInjector {
|
|
451
|
+
constructor(keywords) {
|
|
452
|
+
this.keywords = keywords || {};
|
|
453
|
+
this.injected = [];
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
inject(text, density = 0.15) {
|
|
457
|
+
if (!this.keywords || Object.keys(this.keywords).length === 0) return text;
|
|
458
|
+
|
|
459
|
+
this.injected = [];
|
|
460
|
+
const sentences = text.split(/([.!?]+\s*)/);
|
|
461
|
+
const result = [];
|
|
462
|
+
|
|
463
|
+
for (let i = 0; i < sentences.length; i += 2) {
|
|
464
|
+
let sentence = sentences[i];
|
|
465
|
+
const punctuation = sentences[i + 1] || '';
|
|
466
|
+
|
|
467
|
+
if (sentence && chance(density)) {
|
|
468
|
+
sentence = this.injectIntoSentence(sentence);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
result.push(sentence + punctuation);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
return result.join('');
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
injectIntoSentence(sentence) {
|
|
478
|
+
const categories = Object.keys(this.keywords).filter(cat =>
|
|
479
|
+
this.keywords[cat] && this.keywords[cat].length > 0
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
if (categories.length === 0) return sentence;
|
|
483
|
+
|
|
484
|
+
const category = pick(categories);
|
|
485
|
+
const keyword = pick(this.keywords[category]);
|
|
486
|
+
|
|
487
|
+
if (!keyword) return sentence;
|
|
488
|
+
|
|
489
|
+
this.injected.push(keyword);
|
|
490
|
+
|
|
491
|
+
// Natural injection patterns
|
|
492
|
+
const patterns = [
|
|
493
|
+
s => s.replace(/\b(the|this|that)\s+\w+/i, `$1 ${keyword}`),
|
|
494
|
+
s => s.replace(/\b(error|issue|problem|bug)/i, `${keyword} $1`),
|
|
495
|
+
s => s + ` (I mean ${keyword})`,
|
|
496
|
+
s => s.replace(/\b(broken|working|slow|fast)/i, `$1 with ${keyword}`),
|
|
497
|
+
s => `${keyword} - ` + s
|
|
498
|
+
];
|
|
499
|
+
|
|
500
|
+
return pick(patterns)(sentence);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
getInjected() {
|
|
504
|
+
return [...new Set(this.injected)];
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// ============= Main Generator Class =============
|
|
509
|
+
|
|
510
|
+
class OrganicTextGenerator {
|
|
511
|
+
/**
|
|
512
|
+
* @param {TextGeneratorConfig} [config={}] - Configuration options
|
|
513
|
+
*/
|
|
514
|
+
constructor(config = {}) {
|
|
515
|
+
this.config = {
|
|
516
|
+
tone: 'neu',
|
|
517
|
+
style: 'feedback',
|
|
518
|
+
intensity: 'medium',
|
|
519
|
+
formality: 'casual',
|
|
520
|
+
min: 100,
|
|
521
|
+
max: 500,
|
|
522
|
+
seed: null,
|
|
523
|
+
keywords: null,
|
|
524
|
+
keywordDensity: 0.15,
|
|
525
|
+
typos: false,
|
|
526
|
+
typoRate: 0.02,
|
|
527
|
+
mixedSentiment: true,
|
|
528
|
+
authenticityLevel: 0.3,
|
|
529
|
+
timestamps: false,
|
|
530
|
+
userPersona: false,
|
|
531
|
+
sentimentDrift: 0.2,
|
|
532
|
+
includeMetadata: true,
|
|
533
|
+
specificityLevel: 0.5,
|
|
534
|
+
enableDeduplication: true,
|
|
535
|
+
maxAttempts: 50,
|
|
536
|
+
...config
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
// Initialize seed if provided
|
|
540
|
+
if (this.config.seed) {
|
|
541
|
+
seedrandom(this.config.seed, { global: true });
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Initialize subsystems
|
|
545
|
+
this.thoughtStream = new ThoughtStream();
|
|
546
|
+
this.contextTracker = new ContextTracker();
|
|
547
|
+
this.deduplicator = new NaturalDeduplicator();
|
|
548
|
+
this.voiceConsistency = new VoiceConsistency(this.config.formality);
|
|
549
|
+
this.typoEngine = new NaturalTypoEngine();
|
|
550
|
+
this.keywordInjector = new KeywordInjector(this.config.keywords);
|
|
551
|
+
|
|
552
|
+
// Initialize Tracery grammar as fallback
|
|
553
|
+
this.grammar = tracery.createGrammar(PHRASE_BANK);
|
|
554
|
+
this.grammar.addModifiers(tracery.baseEngModifiers);
|
|
555
|
+
|
|
556
|
+
// Track statistics
|
|
557
|
+
this.stats = {
|
|
558
|
+
generated: 0,
|
|
559
|
+
attempts: 0,
|
|
560
|
+
duplicates: 0,
|
|
561
|
+
failures: 0,
|
|
562
|
+
totalTime: 0
|
|
563
|
+
};
|
|
564
|
+
|
|
565
|
+
// Current generation state
|
|
566
|
+
this.currentTone = this.config.tone;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
next() {
|
|
570
|
+
return this.generateOne();
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Generate a single text item
|
|
575
|
+
* @returns {string|GeneratedText|null} Generated text or null if failed
|
|
576
|
+
*/
|
|
577
|
+
generateOne() {
|
|
578
|
+
const startTime = Date.now();
|
|
579
|
+
|
|
580
|
+
for (let attempt = 0; attempt < this.config.maxAttempts; attempt++) {
|
|
581
|
+
this.stats.attempts++;
|
|
582
|
+
|
|
583
|
+
// Allow sentiment drift
|
|
584
|
+
if (this.config.sentimentDrift > 0 && chance(this.config.sentimentDrift)) {
|
|
585
|
+
this.currentTone = this.driftTone(this.currentTone);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Reset context for new generation
|
|
589
|
+
this.thoughtStream.reset();
|
|
590
|
+
this.contextTracker.reset();
|
|
591
|
+
|
|
592
|
+
// Choose generation strategy
|
|
593
|
+
const strategy = this.selectStrategy();
|
|
594
|
+
let text = null;
|
|
595
|
+
|
|
596
|
+
try {
|
|
597
|
+
switch (strategy) {
|
|
598
|
+
case 'stream':
|
|
599
|
+
text = this.generateStreamOfConsciousness();
|
|
600
|
+
break;
|
|
601
|
+
case 'burst':
|
|
602
|
+
text = this.generateEmotionalBurst();
|
|
603
|
+
break;
|
|
604
|
+
case 'structured':
|
|
605
|
+
text = this.generateStructuredThought();
|
|
606
|
+
break;
|
|
607
|
+
case 'fragment':
|
|
608
|
+
text = this.generateFragmented();
|
|
609
|
+
break;
|
|
610
|
+
case 'narrative':
|
|
611
|
+
text = this.generateNarrative();
|
|
612
|
+
break;
|
|
613
|
+
default:
|
|
614
|
+
text = this.generateHybrid();
|
|
615
|
+
}
|
|
616
|
+
} catch (e) {
|
|
617
|
+
this.stats.failures++;
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (!text || text.length < this.config.min) continue;
|
|
622
|
+
|
|
623
|
+
// Apply enhancements
|
|
624
|
+
text = this.enhance(text);
|
|
625
|
+
|
|
626
|
+
// Fast duplicate check - only check if we have few generations
|
|
627
|
+
if (this.config.enableDeduplication && this.stats.generated < 100 && this.deduplicator.wouldBeDuplicate(text)) {
|
|
628
|
+
this.stats.duplicates++;
|
|
629
|
+
continue;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Length validation
|
|
633
|
+
if (text.length > this.config.max) {
|
|
634
|
+
text = this.smartTruncate(text);
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (text.length >= this.config.min && text.length <= this.config.max) {
|
|
638
|
+
// Success!
|
|
639
|
+
this.stats.generated++;
|
|
640
|
+
this.stats.totalTime += Date.now() - startTime;
|
|
641
|
+
|
|
642
|
+
// Only add to deduplicator if we're still doing duplicate checking
|
|
643
|
+
if (this.config.enableDeduplication && this.stats.generated < 100) {
|
|
644
|
+
this.deduplicator.add(text);
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Return based on metadata preference
|
|
648
|
+
if (this.config.includeMetadata) {
|
|
649
|
+
return this.createTextObject(text);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
return text;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Fallback
|
|
657
|
+
this.stats.failures++;
|
|
658
|
+
return this.generateFallback();
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
selectStrategy() {
|
|
662
|
+
const strategies = {
|
|
663
|
+
support: {
|
|
664
|
+
high: ['burst', 'stream', 'burst'],
|
|
665
|
+
medium: ['structured', 'stream', 'narrative'],
|
|
666
|
+
low: ['structured', 'narrative', 'structured']
|
|
667
|
+
},
|
|
668
|
+
review: {
|
|
669
|
+
high: ['narrative', 'burst', 'stream'],
|
|
670
|
+
medium: ['narrative', 'structured', 'stream'],
|
|
671
|
+
low: ['structured', 'narrative', 'structured']
|
|
672
|
+
},
|
|
673
|
+
chat: {
|
|
674
|
+
high: ['fragment', 'burst', 'stream'],
|
|
675
|
+
medium: ['fragment', 'stream', 'fragment'],
|
|
676
|
+
low: ['fragment', 'structured', 'fragment']
|
|
677
|
+
},
|
|
678
|
+
feedback: {
|
|
679
|
+
high: ['stream', 'burst', 'narrative'],
|
|
680
|
+
medium: ['structured', 'narrative', 'stream'],
|
|
681
|
+
low: ['structured', 'structured', 'narrative']
|
|
682
|
+
},
|
|
683
|
+
search: {
|
|
684
|
+
high: ['fragment', 'fragment', 'burst'],
|
|
685
|
+
medium: ['fragment', 'fragment', 'fragment'],
|
|
686
|
+
low: ['fragment', 'fragment', 'fragment']
|
|
687
|
+
},
|
|
688
|
+
email: {
|
|
689
|
+
high: ['structured', 'narrative', 'stream'],
|
|
690
|
+
medium: ['structured', 'narrative', 'structured'],
|
|
691
|
+
low: ['structured', 'structured', 'narrative']
|
|
692
|
+
},
|
|
693
|
+
forum: {
|
|
694
|
+
high: ['stream', 'narrative', 'burst'],
|
|
695
|
+
medium: ['narrative', 'structured', 'stream'],
|
|
696
|
+
low: ['structured', 'narrative', 'structured']
|
|
697
|
+
}
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
const styleStrategies = strategies[this.config.style] || strategies.feedback;
|
|
701
|
+
const intensityStrategies = styleStrategies[this.config.intensity] || styleStrategies.medium;
|
|
702
|
+
|
|
703
|
+
return pick(intensityStrategies);
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
generateStreamOfConsciousness() {
|
|
707
|
+
const thoughts = [];
|
|
708
|
+
const numThoughts = 2 + Math.floor(Math.random() * 4);
|
|
709
|
+
|
|
710
|
+
for (let i = 0; i < numThoughts; i++) {
|
|
711
|
+
const thought = this.thoughtStream.generateThought(
|
|
712
|
+
this.currentTone,
|
|
713
|
+
this.contextTracker.getContext()
|
|
714
|
+
);
|
|
715
|
+
|
|
716
|
+
if (thought) {
|
|
717
|
+
thoughts.push(thought);
|
|
718
|
+
this.contextTracker.update(thought);
|
|
719
|
+
|
|
720
|
+
// Add interruptions
|
|
721
|
+
if (chance(0.3) && i < numThoughts - 1) {
|
|
722
|
+
thoughts.push(pick(ORGANIC_PATTERNS.interruptions));
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
return this.thoughtStream.connect(thoughts);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
generateEmotionalBurst() {
|
|
731
|
+
const emotion = this.currentTone === 'pos' ? 'excitement' :
|
|
732
|
+
this.currentTone === 'neg' ? 'frustration' : 'confusion';
|
|
733
|
+
|
|
734
|
+
const burst = [];
|
|
735
|
+
|
|
736
|
+
// Opening
|
|
737
|
+
burst.push(pick(ORGANIC_PATTERNS.bursts[emotion].openings));
|
|
738
|
+
|
|
739
|
+
// Core message
|
|
740
|
+
const core = this.grammar.flatten(`#${this.currentTone}_core#`);
|
|
741
|
+
if (emotion === 'frustration' && chance(0.5)) {
|
|
742
|
+
burst.push(core.toUpperCase());
|
|
743
|
+
} else {
|
|
744
|
+
burst.push(core);
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Emphasis
|
|
748
|
+
if (chance(0.6)) {
|
|
749
|
+
burst.push(pick(ORGANIC_PATTERNS.bursts[emotion].emphasis));
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
// Closer
|
|
753
|
+
burst.push(pick(ORGANIC_PATTERNS.bursts[emotion].closers));
|
|
754
|
+
|
|
755
|
+
return burst.filter(Boolean).join(' ');
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
generateStructuredThought() {
|
|
759
|
+
const structure = GENERATION_PATTERNS.structures[this.config.style] ||
|
|
760
|
+
GENERATION_PATTERNS.structures.default;
|
|
761
|
+
|
|
762
|
+
const parts = [];
|
|
763
|
+
|
|
764
|
+
for (const element of structure) {
|
|
765
|
+
if (chance(element.probability)) {
|
|
766
|
+
const content = this.grammar.flatten(element.pattern);
|
|
767
|
+
parts.push(content);
|
|
768
|
+
|
|
769
|
+
if (element.transition && chance(0.4)) {
|
|
770
|
+
parts.push(pick(ORGANIC_PATTERNS.transitions));
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
return parts.join(' ');
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
generateFragmented() {
|
|
779
|
+
const fragments = [];
|
|
780
|
+
const numFragments = this.config.style === 'search' ?
|
|
781
|
+
1 + Math.floor(Math.random() * 3) :
|
|
782
|
+
2 + Math.floor(Math.random() * 4);
|
|
783
|
+
|
|
784
|
+
for (let i = 0; i < numFragments; i++) {
|
|
785
|
+
const type = Math.random();
|
|
786
|
+
|
|
787
|
+
if (type < 0.3) {
|
|
788
|
+
fragments.push(pick(ORGANIC_PATTERNS.fragments.incomplete));
|
|
789
|
+
} else if (type < 0.6) {
|
|
790
|
+
fragments.push(pick(ORGANIC_PATTERNS.fragments.short));
|
|
791
|
+
} else {
|
|
792
|
+
const full = this.grammar.flatten(`#${this.currentTone}_core#`);
|
|
793
|
+
fragments.push(this.breakSentence(full));
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Connect fragments naturally
|
|
798
|
+
return this.connectFragments(fragments);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
generateNarrative() {
|
|
802
|
+
const narrative = GENERATION_PATTERNS.narratives[this.config.style];
|
|
803
|
+
if (!narrative) return this.generateHybrid();
|
|
804
|
+
|
|
805
|
+
const story = [];
|
|
806
|
+
|
|
807
|
+
for (const beat of narrative) {
|
|
808
|
+
if (chance(beat.optional ? 0.6 : 0.9)) {
|
|
809
|
+
const content = this.grammar.flatten(beat.template);
|
|
810
|
+
story.push(content);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
return story.join(' ');
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
generateHybrid() {
|
|
818
|
+
// Mix multiple strategies
|
|
819
|
+
const parts = [];
|
|
820
|
+
|
|
821
|
+
// Opening
|
|
822
|
+
if (chance(0.7)) {
|
|
823
|
+
parts.push(pick(ORGANIC_PATTERNS.openings[this.config.style] || ORGANIC_PATTERNS.openings.default));
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
// Main content
|
|
827
|
+
const main = this.grammar.flatten(`#origin_${this.config.style}_${this.currentTone}#`);
|
|
828
|
+
parts.push(main);
|
|
829
|
+
|
|
830
|
+
// Additional thoughts
|
|
831
|
+
if (chance(0.4)) {
|
|
832
|
+
parts.push(pick(ORGANIC_PATTERNS.addons[this.currentTone]));
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
// Closing
|
|
836
|
+
if (chance(0.6)) {
|
|
837
|
+
parts.push(pick(ORGANIC_PATTERNS.closings[this.currentTone]));
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
return parts.filter(Boolean).join(' ');
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
generateFallback() {
|
|
844
|
+
// Simple fallback when all else fails
|
|
845
|
+
const simple = this.grammar.flatten(`#origin_${this.currentTone}#`);
|
|
846
|
+
return this.enhance(simple);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
enhance(text) {
|
|
850
|
+
// Layer 1: Mixed sentiment
|
|
851
|
+
if (this.config.mixedSentiment && chance(0.3)) {
|
|
852
|
+
text = this.addMixedSentiment(text);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// Layer 2: Keywords
|
|
856
|
+
if (this.config.keywords) {
|
|
857
|
+
text = this.keywordInjector.inject(text, this.config.keywordDensity);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// Layer 3: Authenticity markers
|
|
861
|
+
if (this.config.authenticityLevel > 0) {
|
|
862
|
+
text = this.addAuthenticityMarkers(text);
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
// Layer 4: Specificity
|
|
866
|
+
if (this.config.specificityLevel > 0.5) {
|
|
867
|
+
text = this.addSpecificDetails(text);
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// Layer 5: Voice consistency
|
|
871
|
+
text = this.voiceConsistency.maintain(text);
|
|
872
|
+
|
|
873
|
+
// Layer 6: Typos
|
|
874
|
+
if (this.config.typos) {
|
|
875
|
+
const context = {
|
|
876
|
+
emotional: this.thoughtStream.momentum.emotional,
|
|
877
|
+
mobile: this.config.style === 'chat'
|
|
878
|
+
};
|
|
879
|
+
text = this.typoEngine.apply(text, this.config.typoRate, context);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Layer 7: Timestamps
|
|
883
|
+
if (this.config.timestamps && chance(0.3)) {
|
|
884
|
+
text = this.addTimestamp(text);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Layer 8: User persona
|
|
888
|
+
if (this.config.userPersona && chance(0.4)) {
|
|
889
|
+
text = this.addPersonaMarker(text);
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
return text;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
addMixedSentiment(text) {
|
|
896
|
+
const counter = this.currentTone === 'pos' ? 'neg' :
|
|
897
|
+
this.currentTone === 'neg' ? 'pos' :
|
|
898
|
+
pick(['pos', 'neg']);
|
|
899
|
+
|
|
900
|
+
const addition = pick([
|
|
901
|
+
`That said, ${this.grammar.flatten(`#${counter}_point#`)}`,
|
|
902
|
+
`Although ${this.grammar.flatten(`#${counter}_clause#`)}`,
|
|
903
|
+
`But ${this.grammar.flatten(`#${counter}_short#`)}`
|
|
904
|
+
]);
|
|
905
|
+
|
|
906
|
+
return text + '. ' + addition;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
addAuthenticityMarkers(text) {
|
|
910
|
+
const markers = [];
|
|
911
|
+
|
|
912
|
+
if (chance(this.config.authenticityLevel * 0.3)) {
|
|
913
|
+
markers.push(pick(ORGANIC_PATTERNS.authenticity.selfCorrections));
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
if (chance(this.config.authenticityLevel * 0.3)) {
|
|
917
|
+
markers.push(pick(ORGANIC_PATTERNS.authenticity.fillers));
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
if (chance(this.config.authenticityLevel * 0.2)) {
|
|
921
|
+
markers.push(pick(ORGANIC_PATTERNS.authenticity.asides));
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Insert markers naturally
|
|
925
|
+
for (const marker of markers) {
|
|
926
|
+
const insertPoint = Math.floor(Math.random() * text.length);
|
|
927
|
+
text = text.slice(0, insertPoint) + ' ' + marker + ' ' + text.slice(insertPoint);
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
return text;
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
addSpecificDetails(text) {
|
|
934
|
+
const details = {
|
|
935
|
+
pos: ['saved 2 hours daily', 'reduced costs by 40%', 'loads in under 100ms'],
|
|
936
|
+
neg: ['crashes 3-4 times per day', 'takes 30+ seconds to load', 'error 404 constantly'],
|
|
937
|
+
neu: ['works most of the time', 'about average performance', 'standard functionality']
|
|
938
|
+
};
|
|
939
|
+
|
|
940
|
+
const relevantDetails = details[this.currentTone] || details.neu;
|
|
941
|
+
|
|
942
|
+
if (chance(this.config.specificityLevel)) {
|
|
943
|
+
const detail = pick(relevantDetails);
|
|
944
|
+
text = text.replace(/\.$/, ` - ${detail}.`);
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
return text;
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
addTimestamp(text) {
|
|
951
|
+
const hour = Math.floor(Math.random() * 24);
|
|
952
|
+
const min = Math.floor(Math.random() * 60);
|
|
953
|
+
const timestamp = `[${hour}:${min.toString().padStart(2, '0')}]`;
|
|
954
|
+
|
|
955
|
+
return timestamp + ' ' + text;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
addPersonaMarker(text) {
|
|
959
|
+
const personas = [
|
|
960
|
+
'As a developer, ',
|
|
961
|
+
'As someone who uses this daily, ',
|
|
962
|
+
'Speaking from experience, ',
|
|
963
|
+
'In my 10+ years in tech, ',
|
|
964
|
+
'From a user perspective, '
|
|
965
|
+
];
|
|
966
|
+
|
|
967
|
+
return pick(personas) + text.charAt(0).toLowerCase() + text.slice(1);
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
breakSentence(sentence) {
|
|
971
|
+
const breakPoint = Math.floor(sentence.length * (0.3 + Math.random() * 0.4));
|
|
972
|
+
return sentence.slice(0, breakPoint) + '...';
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
connectFragments(fragments) {
|
|
976
|
+
const connectors = ['... ', ' ', ', ', ' - ', '? ', '... wait ', '.. '];
|
|
977
|
+
return fragments.join(pick(connectors));
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
smartTruncate(text) {
|
|
981
|
+
// Truncate at natural boundary
|
|
982
|
+
const truncated = text.slice(0, this.config.max);
|
|
983
|
+
const lastPeriod = truncated.lastIndexOf('.');
|
|
984
|
+
const lastQuestion = truncated.lastIndexOf('?');
|
|
985
|
+
const lastExclamation = truncated.lastIndexOf('!');
|
|
986
|
+
|
|
987
|
+
const lastPunct = Math.max(lastPeriod, lastQuestion, lastExclamation);
|
|
988
|
+
|
|
989
|
+
if (lastPunct > this.config.min * 0.8) {
|
|
990
|
+
return truncated.slice(0, lastPunct + 1);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
return truncated.slice(0, this.config.max - 3) + '...';
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
driftTone(currentTone) {
|
|
997
|
+
const drifts = {
|
|
998
|
+
pos: chance(0.7) ? 'pos' : chance(0.8) ? 'neu' : 'neg',
|
|
999
|
+
neg: chance(0.7) ? 'neg' : chance(0.8) ? 'neu' : 'pos',
|
|
1000
|
+
neu: chance(0.5) ? 'neu' : chance(0.5) ? 'pos' : 'neg'
|
|
1001
|
+
};
|
|
1002
|
+
|
|
1003
|
+
return drifts[currentTone] || currentTone;
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
createTextObject(text) {
|
|
1007
|
+
const metadata = {
|
|
1008
|
+
style: this.config.style,
|
|
1009
|
+
intensity: this.config.intensity,
|
|
1010
|
+
formality: this.config.formality
|
|
1011
|
+
};
|
|
1012
|
+
|
|
1013
|
+
if (this.config.includeMetadata) {
|
|
1014
|
+
// Use fast sentiment estimation instead of full analysis
|
|
1015
|
+
const negWords = (text.match(/\b(broken|terrible|awful|bad|crash|error|fail|bug)\b/gi) || []).length;
|
|
1016
|
+
const posWords = (text.match(/\b(great|excellent|amazing|good|love|perfect|works)\b/gi) || []).length;
|
|
1017
|
+
metadata.sentimentScore = negWords > posWords ? -1 : posWords > negWords ? 1 : 0;
|
|
1018
|
+
|
|
1019
|
+
if (this.config.timestamps) {
|
|
1020
|
+
metadata.timestamp = new Date().toISOString();
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
if (this.config.userPersona) {
|
|
1024
|
+
metadata.persona = {
|
|
1025
|
+
role: pick(['developer', 'designer', 'manager', 'user']),
|
|
1026
|
+
experience: pick(['junior', 'senior', 'expert'])
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
const injected = this.keywordInjector.getInjected();
|
|
1031
|
+
if (injected.length > 0) {
|
|
1032
|
+
metadata.injectedKeywords = injected;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
metadata.readabilityScore = this.calculateReadability(text);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
return {
|
|
1039
|
+
text,
|
|
1040
|
+
tone: this.currentTone,
|
|
1041
|
+
metadata
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
calculateReadability(text) {
|
|
1046
|
+
const words = text.split(/\s+/).length;
|
|
1047
|
+
const sentences = text.split(/[.!?]+/).length;
|
|
1048
|
+
const syllables = text.split(/\s+/).reduce((sum, word) =>
|
|
1049
|
+
sum + this.countSyllables(word), 0);
|
|
1050
|
+
|
|
1051
|
+
const score = 206.835 - 1.015 * (words / sentences) - 84.6 * (syllables / words);
|
|
1052
|
+
return Math.max(0, Math.min(100, Math.round(score)));
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
countSyllables(word) {
|
|
1056
|
+
word = word.toLowerCase().replace(/[^a-z]/g, '');
|
|
1057
|
+
const vowels = word.match(/[aeiou]/g);
|
|
1058
|
+
return vowels ? Math.max(1, vowels.length) : 1;
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
/**
|
|
1062
|
+
* Generate multiple text items in batch
|
|
1063
|
+
* @param {TextBatchOptions} options - Batch generation options
|
|
1064
|
+
* @returns {(string|GeneratedText|SimpleGeneratedText)[]} Array of generated text items
|
|
1065
|
+
*/
|
|
1066
|
+
generateBatch(options) {
|
|
1067
|
+
const {
|
|
1068
|
+
n = 10,
|
|
1069
|
+
returnType = 'strings',
|
|
1070
|
+
tone = this.config.tone,
|
|
1071
|
+
related = false,
|
|
1072
|
+
sharedContext = null
|
|
1073
|
+
} = options;
|
|
1074
|
+
|
|
1075
|
+
const results = [];
|
|
1076
|
+
const startTime = Date.now();
|
|
1077
|
+
|
|
1078
|
+
// Reset for new batch
|
|
1079
|
+
this.currentTone = tone;
|
|
1080
|
+
|
|
1081
|
+
// Generate shared context if related
|
|
1082
|
+
let context = sharedContext;
|
|
1083
|
+
if (related && !context) {
|
|
1084
|
+
const contexts = ['new feature', 'recent update', 'pricing change', 'UI redesign', 'performance issues'];
|
|
1085
|
+
context = pick(contexts);
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
for (let i = 0; i < n; i++) {
|
|
1089
|
+
let item = this.generateOne();
|
|
1090
|
+
|
|
1091
|
+
if (!item) {
|
|
1092
|
+
this.stats.failures++;
|
|
1093
|
+
continue;
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
// Add shared context if related
|
|
1097
|
+
if (related && context) {
|
|
1098
|
+
const text = typeof item === 'string' ? item : item.text;
|
|
1099
|
+
const contextualText = this.addSharedContext(text, context);
|
|
1100
|
+
|
|
1101
|
+
if (typeof item === 'string') {
|
|
1102
|
+
item = contextualText;
|
|
1103
|
+
} else {
|
|
1104
|
+
item.text = contextualText;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// Format based on return type
|
|
1109
|
+
if (returnType === 'strings') {
|
|
1110
|
+
results.push(typeof item === 'string' ? item : item.text);
|
|
1111
|
+
} else {
|
|
1112
|
+
results.push(typeof item === 'string' ? { text: item, tone } : item);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
this.stats.totalTime += Date.now() - startTime;
|
|
1117
|
+
|
|
1118
|
+
return results;
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
addSharedContext(text, context) {
|
|
1122
|
+
const templates = [
|
|
1123
|
+
`About the ${context}: ${text}`,
|
|
1124
|
+
`${text} (regarding the ${context})`,
|
|
1125
|
+
`Re: ${context} - ${text}`,
|
|
1126
|
+
`${text}. This is about the ${context}.`
|
|
1127
|
+
];
|
|
1128
|
+
|
|
1129
|
+
return pick(templates);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
/**
|
|
1133
|
+
* Get generation statistics
|
|
1134
|
+
* @returns {TextGeneratorStats} Performance statistics
|
|
1135
|
+
*/
|
|
1136
|
+
getStats() {
|
|
1137
|
+
const avgTime = this.stats.generated > 0 ?
|
|
1138
|
+
this.stats.totalTime / this.stats.generated : 0;
|
|
1139
|
+
|
|
1140
|
+
return {
|
|
1141
|
+
config: this.config,
|
|
1142
|
+
generatedCount: this.stats.generated,
|
|
1143
|
+
duplicateCount: this.stats.duplicates,
|
|
1144
|
+
failedCount: this.stats.failures,
|
|
1145
|
+
avgGenerationTime: avgTime,
|
|
1146
|
+
totalGenerationTime: this.stats.totalTime
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
// ============= Public API =============
|
|
1152
|
+
|
|
1153
|
+
/**
|
|
1154
|
+
* Creates a new text generator instance
|
|
1155
|
+
* @param {TextGeneratorConfig} [config={}] - Configuration options for the generator
|
|
1156
|
+
* @returns {OrganicTextGenerator} Text generator instance
|
|
1157
|
+
*/
|
|
1158
|
+
export function createGenerator(config = {}) {
|
|
1159
|
+
return new OrganicTextGenerator(config);
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
/**
|
|
1163
|
+
* Generate a batch of text items directly (standalone function)
|
|
1164
|
+
* @param {TextGeneratorConfig & TextBatchOptions} options - Combined generator config and batch options
|
|
1165
|
+
* @returns {(string|GeneratedText|SimpleGeneratedText)[]} Array of generated text items
|
|
1166
|
+
*/
|
|
1167
|
+
export function generateBatch(options) {
|
|
1168
|
+
const { n, returnType, tone, related, sharedContext, ...config } = options;
|
|
1169
|
+
const generator = new OrganicTextGenerator(config);
|
|
1170
|
+
return generator.generateBatch({ n, returnType, tone, related, sharedContext });
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
export default OrganicTextGenerator;
|