Governed substrate for autonomous agents: scoped identity (passports), audited actions, MCP workspace. Infra IPs and secrets redacted for public release.
637 lines
32 KiB
JavaScript
637 lines
32 KiB
JavaScript
/**
|
||
* PES Soul — Babble Engine v4 (ЖИВОЙ ГОЛОС)
|
||
* v3 → v4 changes:
|
||
* - Emotion blends (two emotions → hybrid sound)
|
||
* - Affinity tracking (imprint actually works — learns from praise)
|
||
* - Context-aware generation (time of day, silence duration, conversation flow)
|
||
* - Micro-expressions (tiny sounds between main phrases)
|
||
* - Breath patterns (pauses, inhales, sighs)
|
||
* - Idle variety (20+ unique idle babbles)
|
||
*
|
||
* Level gates: 0=tier0 2=combo 4=tier1 6=tier2 10=tier3 15=invention 20=advanced 40=crypto
|
||
*/
|
||
|
||
const CONSONANTS = ['г','р','м','х','ф','в','т','с','к','н','п','б','д','л','ш','ж'];
|
||
const VOWELS = ['а','у','ы','о','и','е','э','ю'];
|
||
const NOISE = ['3','7','0','z','q','x','ц','щ','~','.','-'];
|
||
|
||
const ANIMAL_SOUNDS = {
|
||
tier0: ['гав','аф','мр','хм','рр','уу','ыы','хн'],
|
||
tier1: ['тяф','ваф','рру','скс','мрр','вуф','ууу','ыыы','скууу'],
|
||
tier2: ['мням','хрум','аууу','руу-руу','гяв','яп','хааа','пфф'],
|
||
tier3: ['рррРРР','ыыыЫЫЫ','ГАВВВ','ААААА','вввуууф','скууу~'],
|
||
};
|
||
const ABSTRACT_SYMBOLS = ['◈','∿','≋','⟡','⊹','⟁','∞','◌','⊕','⑂'];
|
||
const INTENSITY_MARKS = {
|
||
low: ['...','~','.','..'], mid: ['!','~','-',''],
|
||
high: ['!!','!!!','!!','ВВ'], extreme: ['!!!!','!!!','АААА','!!!!'],
|
||
};
|
||
|
||
const EMOTION_INSTRUMENTS = {
|
||
happy: { tempo:'fast', pitch:'high', pattern:'staccato', connector:'! ' },
|
||
playful: { tempo:'fast', pitch:'high', pattern:'bounce', connector:'-' },
|
||
greeting_frenzy: { tempo:'burst', pitch:'high', pattern:'chaos', connector:' ' },
|
||
butt_wiggle: { tempo:'fast', pitch:'mid', pattern:'wiggle', connector:'~' },
|
||
zoomies: { tempo:'burst', pitch:'high', pattern:'repeat', connector:'!!' },
|
||
excited: { tempo:'fast', pitch:'high', pattern:'staccato', connector:'! ' },
|
||
idle: { tempo:'slow', pitch:'low', pattern:'smooth', connector:'~ ' },
|
||
content: { tempo:'slow', pitch:'mid', pattern:'smooth', connector:'~' },
|
||
nap: { tempo:'slow', pitch:'low', pattern:'fade', connector:'...' },
|
||
sleep: { tempo:'slow', pitch:'low', pattern:'fade', connector:'... ' },
|
||
sad: { tempo:'slow', pitch:'low', pattern:'descend', connector:'.. ' },
|
||
lonely: { tempo:'slow', pitch:'low', pattern:'echo', connector:'... ' },
|
||
scared: { tempo:'halt', pitch:'low', pattern:'stutter', connector:'. ' },
|
||
anxious: { tempo:'mid', pitch:'mid', pattern:'stutter', connector:'- ' },
|
||
food_obsessed: { tempo:'fast', pitch:'mid', pattern:'repeat', connector:' ' },
|
||
hungry: { tempo:'mid', pitch:'mid', pattern:'repeat', connector:'! ' },
|
||
alert: { tempo:'mid', pitch:'high', pattern:'staccato', connector:'! ' },
|
||
angry: { tempo:'fast', pitch:'high', pattern:'sharp', connector:'!' },
|
||
bark: { tempo:'burst', pitch:'high', pattern:'sharp', connector:'! ' },
|
||
curious: { tempo:'mid', pitch:'mid', pattern:'question', connector:'? ' },
|
||
puzzle_solving: { tempo:'slow', pitch:'mid', pattern:'question', connector:'.. ' },
|
||
dramatic_tantrum:{ tempo:'burst', pitch:'high', pattern:'chaos', connector:'!! ' },
|
||
sulking: { tempo:'halt', pitch:'low', pattern:'minimal', connector:'. ' },
|
||
stubborn_refuse: { tempo:'halt', pitch:'mid', pattern:'minimal', connector:'.' },
|
||
};
|
||
|
||
const RHYTHM_PATTERNS = {
|
||
staccato: a => { a[0]=a[0].toUpperCase(); if(a.length>2) a[a.length-1]=a[a.length-1].toUpperCase(); return a; },
|
||
bounce: a => a.map((x,i) => i%2===0 ? x : x.toLowerCase()),
|
||
chaos: a => a.map(x => x.toUpperCase()),
|
||
smooth: a => a.map(x => x.toLowerCase()),
|
||
fade: a => a.map((x,i) => i===0 ? x.toUpperCase() : i===a.length-1 ? x.toLowerCase()+'...' : x.toLowerCase()),
|
||
descend: a => a.map((x,i) => x.toLowerCase()+'.'.repeat(Math.min(i,3))),
|
||
echo: a => { const b=a[0]; return a.map((x,i) => i===0 ? x : b.toLowerCase()); },
|
||
stutter: a => a.map(x => x.length<=2 ? x+'-'+x : x.charAt(0)+'-'+x),
|
||
question: a => a.map((x,i) => i===a.length-1 ? x+'?..' : x),
|
||
repeat: a => { if(a.length>=2) a[a.length-1]=a[0]; return a; },
|
||
sharp: a => a.map(x => x.length>3 ? x.slice(0,3).toUpperCase() : x.toUpperCase()),
|
||
wiggle: a => a.map(x => '~'+x+'~'),
|
||
minimal: a => a.slice(0, Math.max(1, Math.floor(a.length/2))),
|
||
// v4: new patterns
|
||
crescendo: a => a.map((x,i) => { const r=i/Math.max(1,a.length-1); return r<0.3?x.toLowerCase() : r<0.7?x : x.toUpperCase(); }),
|
||
pulse: a => a.map((x,i) => i%3===1 ? x.toUpperCase() : x.toLowerCase()),
|
||
whisper: a => a.map(x => x.toLowerCase().replace(/(.)/g, '$1·')),
|
||
};
|
||
|
||
const TIER_GATES = {
|
||
babble_tier0:0, combo_phrases:2, babble_tier1:4, babble_tier2:6,
|
||
babble_tier3:10, sound_invention:15, advanced_babble:20, crypto_language:40,
|
||
};
|
||
|
||
// ── v4: BREATH PATTERNS ────────────────────────────────────────
|
||
// Small sounds that mimic breathing between phrases
|
||
const BREATH_PATTERNS = {
|
||
calm: ['~', '...', '~~', ' '],
|
||
excited: ['!', '-', '!!', ' '],
|
||
tired: ['...', '....', '.. ..', 'zzz'],
|
||
curious: ['?', '..?', '~?', ' '],
|
||
tense: ['.', '..', '-', '...'],
|
||
};
|
||
|
||
// ── v4: MICRO-EXPRESSIONS ──────────────────────────────────────
|
||
// Tiny additions that make babble feel alive
|
||
const MICRO_EXPR = {
|
||
sniff: ['*сн*', '*фрр*', '*нюх*'],
|
||
yawn: ['*ааа~*', '*ммм~*', '*хаа~*'],
|
||
sigh: ['*хх..*', '*фф..*', '*пфх..*'],
|
||
tail_wag: ['~~', '~!', '~~~'],
|
||
ear_twitch:['👂', '📡', ''],
|
||
paw_tap: ['🐾', '🐾🐾', ''],
|
||
nose_boop: ['👃💨', ''],
|
||
purr: ['мрр~', 'ррр~', 'ммм~'],
|
||
whimper: ['скс..', 'хнн..', 'ыыы..'],
|
||
};
|
||
|
||
// ── v4: EMOTION BLEND MAP ──────────────────────────────────────
|
||
// When two emotions mix, which instrument settings to use
|
||
const EMOTION_BLENDS = {
|
||
'happy+curious': { tempo:'fast', pitch:'high', pattern:'question', connector:'! ' },
|
||
'playful+alert': { tempo:'fast', pitch:'high', pattern:'staccato', connector:'! ' },
|
||
'content+curious': { tempo:'mid', pitch:'mid', pattern:'question', connector:'~ ' },
|
||
'sad+lonely': { tempo:'slow', pitch:'low', pattern:'echo', connector:'... ' },
|
||
'scared+curious': { tempo:'mid', pitch:'mid', pattern:'stutter', connector:'..? ' },
|
||
'happy+excited': { tempo:'burst', pitch:'high', pattern:'crescendo', connector:'! ' },
|
||
'idle+hungry': { tempo:'mid', pitch:'mid', pattern:'pulse', connector:'~ ' },
|
||
'playful+food_obsessed': { tempo:'fast', pitch:'mid', pattern:'bounce', connector:'-' },
|
||
'alert+curious': { tempo:'mid', pitch:'high', pattern:'staccato', connector:'? ' },
|
||
'content+happy': { tempo:'mid', pitch:'mid', pattern:'smooth', connector:'~ ' },
|
||
'lonely+scared': { tempo:'halt', pitch:'low', pattern:'whisper', connector:'... ' },
|
||
};
|
||
|
||
|
||
class BabbleEngine {
|
||
constructor(seed = 0.5, unlockedSounds = []) {
|
||
this.seed = seed;
|
||
this._rng = this._mkRng(seed);
|
||
this._unlockedSounds = unlockedSounds;
|
||
this.ownerTopEmoji = [];
|
||
this.soundMemory = [];
|
||
this.inventedSounds = [];
|
||
this._invCtr = 0;
|
||
this.favC = this._pickN(CONSONANTS, 6, seed);
|
||
this.favV = this._pickN(VOWELS, 4, seed + 0.1);
|
||
this.signatureSound = this.favC[0]+this.favV[0]+this.favC[1]+this.favV[1]+this.favC[0];
|
||
|
||
// v4: Affinity tracking — sounds that got positive feedback
|
||
this.affinityMap = {}; // { soundFragment: score }
|
||
this._lastGeneratedAtoms = [];
|
||
|
||
// v4: Context state
|
||
this._lastEmotionTs = Date.now();
|
||
this._consecutiveEmotion = null;
|
||
this._consecutiveCount = 0;
|
||
this._conversationMomentum = 0; // 0=cold, 1=hot (rapid back-and-forth)
|
||
this._lastGenerateTs = 0;
|
||
}
|
||
|
||
// ── RNG ────────────────────────────────────────────────────
|
||
_mkRng(s) { let v=Math.floor(s*2147483647)||1; return ()=>{ v=(v*16807)%2147483647; return(v-1)/2147483646; }; }
|
||
_rand() { return (this._rng()+Math.random())/2; }
|
||
_pick(a) { return a[Math.floor(this._rand()*a.length)]; }
|
||
_pickN(arr, n, seed) {
|
||
const s=[...arr]; let v=Math.floor(seed*1000);
|
||
for(let i=s.length-1;i>0;i--){ v=(v*16807)%2147483647; const j=v%(i+1); [s[i],s[j]]=[s[j],s[i]]; }
|
||
return s.slice(0,n);
|
||
}
|
||
|
||
// ── Tier check ─────────────────────────────────────────────
|
||
_has(level, tier) {
|
||
if (level < (TIER_GATES[tier]??999)) return false;
|
||
return !this._unlockedSounds.length || this._unlockedSounds.includes(tier);
|
||
}
|
||
|
||
// ── Atom generation ────────────────────────────────────────
|
||
_atom(level, inst) {
|
||
const r = this._rand();
|
||
|
||
// v4: Affinity boost — prefer sounds that got positive feedback
|
||
if (Object.keys(this.affinityMap).length > 0 && r < 0.12) {
|
||
const top = Object.entries(this.affinityMap)
|
||
.filter(([,v]) => v > 0.3)
|
||
.sort((a,b) => b[1]-a[1]);
|
||
if (top.length > 0) {
|
||
const chosen = top[Math.floor(this._rand() * Math.min(3, top.length))];
|
||
if (chosen) return chosen[0];
|
||
}
|
||
}
|
||
|
||
if (this._has(level,'sound_invention') && this.inventedSounds.length && r<0.15) return this._pick(this.inventedSounds);
|
||
if (level>=4 && this._rand()<0.08) return this.signatureSound;
|
||
|
||
if (this._has(level,'crypto_language')) {
|
||
if (r<0.15) return this._pick(ABSTRACT_SYMBOLS)+this._pick(ANIMAL_SOUNDS.tier2)+this._pick(ABSTRACT_SYMBOLS);
|
||
if (r<0.30) return this._pick(ANIMAL_SOUNDS.tier3)+'~'+this._pick(ABSTRACT_SYMBOLS);
|
||
if (r<0.45) return this._pick(ABSTRACT_SYMBOLS)+this._pick(ANIMAL_SOUNDS.tier1);
|
||
}
|
||
if (this._has(level,'advanced_babble')) {
|
||
if (r<0.25) return this._pick(ANIMAL_SOUNDS.tier2)+'-'+this._pick(ANIMAL_SOUNDS.tier1);
|
||
if (r<0.40) return this._pick(ANIMAL_SOUNDS.tier3);
|
||
if (r<0.55) return this._pick(ANIMAL_SOUNDS.tier1)+' '+this._pick(ANIMAL_SOUNDS.tier1);
|
||
}
|
||
if (this._has(level,'babble_tier3')) {
|
||
if (inst?.pitch==='high' && r<0.30) return this._pick(ANIMAL_SOUNDS.tier1).toUpperCase();
|
||
if (r<0.25) return this._pick(ANIMAL_SOUNDS.tier1);
|
||
if (r<0.45) return this._pick(ANIMAL_SOUNDS.tier2);
|
||
if (r<0.60) return this._pick(ANIMAL_SOUNDS.tier0)+'-'+this._pick(ANIMAL_SOUNDS.tier0);
|
||
return this._pick(ANIMAL_SOUNDS.tier1)+this._pick(ANIMAL_SOUNDS.tier0);
|
||
}
|
||
if (this._has(level,'babble_tier2')) {
|
||
if (inst?.pitch==='high') {
|
||
if (r<0.35) return this._pick(ANIMAL_SOUNDS.tier0);
|
||
if (r<0.60) return this._pick(ANIMAL_SOUNDS.tier1);
|
||
return this._pick(this.favC).toUpperCase()+this._pick(this.favV);
|
||
}
|
||
if (inst?.pitch==='low') {
|
||
if (r<0.30) return this._pick(this.favV).repeat(2)+'~';
|
||
if (r<0.55) return this._pick(ANIMAL_SOUNDS.tier0);
|
||
return this._pick(ANIMAL_SOUNDS.tier1);
|
||
}
|
||
if (r<0.35) return this._pick(ANIMAL_SOUNDS.tier0);
|
||
if (r<0.55) return this._pick(ANIMAL_SOUNDS.tier1);
|
||
return this._pick(this.favC)+this._pick(this.favV)+this._pick(this.favV);
|
||
}
|
||
if (this._has(level,'babble_tier1')) {
|
||
if (r<0.25) return this._pick(this.favC)+this._pick(this.favV);
|
||
if (r<0.45) return this._pick(this.favV).repeat(Math.floor(this._rand()*2)+1);
|
||
if (r<0.65) return this._pick(this.favC)+this._pick(this.favV)+this._pick(this.favC);
|
||
if (r<0.80) return this._pick(ANIMAL_SOUNDS.tier0);
|
||
return this._pick(this.favC)+this._pick(NOISE.slice(5));
|
||
}
|
||
if (this._has(level,'combo_phrases')) {
|
||
if (r<0.40) return this._pick(this.favC)+this._pick(this.favV);
|
||
if (r<0.65) return this._pick(this.favV)+this._pick(this.favC);
|
||
return this._pick(this.favC).repeat(2)+this._pick(this.favV);
|
||
}
|
||
// tier0
|
||
if (r<0.25) return this._pick(this.favC);
|
||
if (r<0.45) return this._pick(this.favV);
|
||
if (r<0.60) return this._pick(this.favV).repeat(2);
|
||
if (r<0.80) return this._pick(NOISE);
|
||
return this._pick(this.favC).repeat(Math.floor(this._rand()*2)+1);
|
||
}
|
||
|
||
// ── Assembly ───────────────────────────────────────────────
|
||
_inst(em) { return EMOTION_INSTRUMENTS[em]||EMOTION_INSTRUMENTS.idle; }
|
||
_rhythm(atoms, inst) { const fn=RHYTHM_PATTERNS[inst.pattern]; return fn ? fn([...atoms]) : atoms; }
|
||
|
||
_assemble(atoms, inst, intensity) {
|
||
const parts = this._rhythm(atoms, inst);
|
||
let r = inst.tempo==='burst' ? parts.join('') : inst.tempo==='halt' ? parts.join('... ') : parts.join(inst.connector);
|
||
const k = intensity<0.3?'low':intensity<0.6?'mid':intensity<0.85?'high':'extreme';
|
||
return r + this._pick(INTENSITY_MARKS[k]);
|
||
}
|
||
|
||
_weaveEmoji(text, level) {
|
||
if (!this.ownerTopEmoji?.length || this._rand()>Math.min(0.4+level*0.01,0.8)) return text;
|
||
const e=this._pick(this.ownerTopEmoji), p=this._rand();
|
||
if (p<0.3) return e+' '+text;
|
||
if (p<0.7) return text+' '+e;
|
||
const parts=text.split(' '); parts.splice(Math.floor(this._rand()*parts.length),0,e); return parts.join(' ');
|
||
}
|
||
|
||
_addTail(text, emotion, level) {
|
||
if (level<2||this._rand()>0.35) return text;
|
||
const i=this._inst(emotion);
|
||
if (i.tempo==='fast'||i.tempo==='burst') return text+'\n'+this._pick(['🐾?','🐾!','🐾~','♡?']);
|
||
if (i.pitch==='low') return text+'\n'+this._pick(['🐾...','♡..','🐾~']);
|
||
return text+'\n'+this._pick(['🐾','♡','🐾?']);
|
||
}
|
||
|
||
// ── v4: Micro-expression injection ──────────────────────────
|
||
_addMicroExpr(text, emotion, level, intensity) {
|
||
if (level < 3 || this._rand() > 0.35) return text;
|
||
|
||
let type;
|
||
if (['nap','sleep','content'].includes(emotion)) type = 'purr';
|
||
else if (['scared','sad','lonely'].includes(emotion)) type = 'whimper';
|
||
else if (['curious','puzzle_solving','alert'].includes(emotion)) type = 'sniff';
|
||
else if (['idle'].includes(emotion) && intensity < 0.3) type = 'yawn';
|
||
else if (['idle','content'].includes(emotion) && this._rand() < 0.3) type = 'sigh';
|
||
else if (['happy','playful','greeting_frenzy','zoomies'].includes(emotion)) type = 'tail_wag';
|
||
else if (['food_obsessed','hungry'].includes(emotion)) type = 'sniff';
|
||
else type = this._pick(['paw_tap','ear_twitch','nose_boop']);
|
||
|
||
const micro = this._pick(MICRO_EXPR[type] || ['']);
|
||
if (!micro) return text;
|
||
|
||
// Place micro-expression: before, between, or after
|
||
const pos = this._rand();
|
||
if (pos < 0.3) return micro + ' ' + text;
|
||
if (pos < 0.6) {
|
||
const parts = text.split(' ');
|
||
if (parts.length > 2) {
|
||
const idx = 1 + Math.floor(this._rand() * (parts.length - 1));
|
||
parts.splice(idx, 0, micro);
|
||
return parts.join(' ');
|
||
}
|
||
}
|
||
return text + ' ' + micro;
|
||
}
|
||
|
||
// ── v4: Breath pattern between phrases ──────────────────────
|
||
_addBreath(text, emotion, intensity) {
|
||
if (this._rand() > 0.25) return text;
|
||
|
||
let breathType;
|
||
if (['happy','playful','excited','zoomies','greeting_frenzy'].includes(emotion)) breathType = 'excited';
|
||
else if (['nap','sleep','idle','content'].includes(emotion)) breathType = 'calm';
|
||
else if (['curious','puzzle_solving','alert'].includes(emotion)) breathType = 'curious';
|
||
else if (['scared','anxious','sad','lonely'].includes(emotion)) breathType = 'tense';
|
||
else breathType = 'calm';
|
||
|
||
const breath = this._pick(BREATH_PATTERNS[breathType]);
|
||
const parts = text.split(' ');
|
||
if (parts.length < 3) return text;
|
||
|
||
// Insert breath after ~40% of the phrase
|
||
const idx = Math.floor(parts.length * 0.4);
|
||
parts.splice(idx, 0, breath);
|
||
return parts.join(' ');
|
||
}
|
||
|
||
// ── v4: Emotion blend ──────────────────────────────────────
|
||
_blendedInstrument(emotion1, emotion2) {
|
||
const key1 = `${emotion1}+${emotion2}`;
|
||
const key2 = `${emotion2}+${emotion1}`;
|
||
return EMOTION_BLENDS[key1] || EMOTION_BLENDS[key2] || null;
|
||
}
|
||
|
||
// ── Sound invention (15+) ─────────────────────────────────
|
||
inventSound() {
|
||
this._invCtr++;
|
||
if (this._invCtr<12||this._rand()>0.2) return null;
|
||
this._invCtr=0;
|
||
const c=this.favC, v=this.favV, t=this._rand();
|
||
let s;
|
||
if (t<0.25) s=c[0]+v[0]+c[1]+'-'+c[2]+v[1]+c[0];
|
||
else if (t<0.50) s=v[0].repeat(2)+c[0]+v[1]+c[1];
|
||
else if (t<0.75) s=c[0].repeat(3)+v[0].repeat(2);
|
||
else s=this.signatureSound+'~'+v[this._rand()<0.5?0:1];
|
||
if (!this.inventedSounds.includes(s)) {
|
||
this.inventedSounds.push(s);
|
||
if (this.inventedSounds.length>12) this.inventedSounds.shift();
|
||
return s;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
// ── generate ───────────────────────────────────────────────
|
||
generate(level=0, emotion='idle', intensity=0.5) {
|
||
const inst=this._inst(emotion);
|
||
const invented=this._has(level,'sound_invention') ? this.inventSound() : null;
|
||
|
||
// v4: Track consecutive same-emotion to add variety
|
||
if (emotion === this._consecutiveEmotion) {
|
||
this._consecutiveCount++;
|
||
} else {
|
||
this._consecutiveEmotion = emotion;
|
||
this._consecutiveCount = 0;
|
||
}
|
||
|
||
// v4: Conversation momentum (time between generates)
|
||
const now = Date.now();
|
||
const gap = now - this._lastGenerateTs;
|
||
if (gap < 10000) this._conversationMomentum = Math.min(1, this._conversationMomentum + 0.15);
|
||
else if (gap < 60000) this._conversationMomentum = Math.max(0, this._conversationMomentum - 0.1);
|
||
else this._conversationMomentum = 0;
|
||
this._lastGenerateTs = now;
|
||
|
||
let base = level<2?2 : level<6?3 : level<15?4 : level<30?5 : 6;
|
||
if (inst.tempo==='burst') base=Math.max(base,4);
|
||
if (inst.tempo==='halt') base=Math.max(1,base-2);
|
||
|
||
// v4: High momentum = slightly longer phrases
|
||
if (this._conversationMomentum > 0.5) base = Math.min(base + 1, 7);
|
||
|
||
const count=Math.max(1,Math.round(base*(0.7+intensity*0.6)));
|
||
|
||
const atoms=[];
|
||
if (level>=4 && this._rand()<0.2) atoms.push(this.signatureSound);
|
||
for (let i=atoms.length; i<count; i++) {
|
||
if (invented && i===Math.floor(count/2)) atoms.push(invented);
|
||
else atoms.push(this._atom(level,inst));
|
||
}
|
||
|
||
// v4: Track atoms for affinity
|
||
this._lastGeneratedAtoms = [...atoms];
|
||
|
||
let result=this._assemble(atoms,inst,intensity);
|
||
result=this._weaveEmoji(result,level);
|
||
|
||
// v4: Add breath pattern
|
||
result=this._addBreath(result, emotion, intensity);
|
||
|
||
// v4: Add micro-expression
|
||
result=this._addMicroExpr(result, emotion, level, intensity);
|
||
|
||
result=this._addTail(result,emotion,level);
|
||
|
||
// v4: If same emotion 3+ times in a row, force variety
|
||
if (this._consecutiveCount >= 3 && this.soundMemory.length > 0) {
|
||
// Shift some atoms to break repetition
|
||
atoms[0] = this._atom(level, inst);
|
||
if (atoms.length > 2) atoms[atoms.length-1] = this._atom(level, inst);
|
||
result = this._assemble(atoms, inst, intensity);
|
||
result = this._weaveEmoji(result, level);
|
||
result = this._addBreath(result, emotion, intensity);
|
||
result = this._addMicroExpr(result, emotion, level, intensity);
|
||
result = this._addTail(result, emotion, level);
|
||
}
|
||
|
||
if (this.soundMemory.includes(result)) {
|
||
atoms[Math.floor(this._rand()*atoms.length)]=this._atom(level,inst);
|
||
result=this._assemble(atoms,inst,intensity);
|
||
result=this._weaveEmoji(result,level);
|
||
result=this._addBreath(result, emotion, intensity);
|
||
result=this._addMicroExpr(result, emotion, level, intensity);
|
||
result=this._addTail(result,emotion,level);
|
||
}
|
||
this.soundMemory.push(result);
|
||
if (this.soundMemory.length>20) this.soundMemory.shift();
|
||
return result;
|
||
}
|
||
|
||
// ── generateReply ──────────────────────────────────────────
|
||
generateReply(level, emotion, intensity, category) {
|
||
switch(category) {
|
||
case 'greeting': return this.generate(level,'greeting_frenzy',Math.min(intensity+0.3,1))+' '+this._pick(['♡','♡♡','💛','']);
|
||
case 'praise': return this.generate(level,'happy',Math.min(intensity+0.2,1))+' '+this._pick(['♡♡','💛💛💛','✨♡']);
|
||
case 'scold': return this.generate(level,'scared',Math.max(intensity-0.3,0.1))+' '+this._pick(['💧','...','']);
|
||
case 'feed': return this.generate(level,'food_obsessed',Math.min(intensity+0.3,1));
|
||
default: return this.generate(level,emotion,intensity);
|
||
}
|
||
}
|
||
|
||
// ── v4: generateBlended — two emotions at once ──────────────
|
||
generateBlended(level, emotion1, emotion2, intensity) {
|
||
const blended = this._blendedInstrument(emotion1, emotion2);
|
||
if (!blended) {
|
||
// No blend defined — generate both and merge
|
||
const a = this.generate(level, emotion1, intensity);
|
||
const b = this.generate(level, emotion2, intensity * 0.6);
|
||
return a + ' ' + this._pick(['~','...','—','']) + ' ' + b;
|
||
}
|
||
|
||
// Use blended instrument
|
||
const invented = this._has(level,'sound_invention') ? this.inventSound() : null;
|
||
let base = level<2?2 : level<6?3 : level<15?4 : level<30?5 : 6;
|
||
if (blended.tempo==='burst') base=Math.max(base,4);
|
||
const count = Math.max(2, Math.round(base*(0.7+intensity*0.6)));
|
||
|
||
const atoms = [];
|
||
// Mix atoms from both emotion instruments
|
||
const inst1 = this._inst(emotion1);
|
||
const inst2 = this._inst(emotion2);
|
||
for (let i = 0; i < count; i++) {
|
||
if (invented && i === Math.floor(count/2)) atoms.push(invented);
|
||
else atoms.push(this._atom(level, this._rand() < 0.6 ? inst1 : inst2));
|
||
}
|
||
|
||
let result = this._assemble(atoms, blended, intensity);
|
||
result = this._weaveEmoji(result, level);
|
||
result = this._addBreath(result, emotion1, intensity);
|
||
result = this._addMicroExpr(result, emotion1, level, intensity);
|
||
result = this._addTail(result, emotion1, level);
|
||
return result;
|
||
}
|
||
|
||
// ── v4: generateIdle — rich idle babble (not repetitive) ────
|
||
generateIdle(level, vitals = {}) {
|
||
const { energy = 0.5, hunger = 0.2, loneliness = 0.1, curiosity = 0.5 } = vitals;
|
||
|
||
// Pick sub-emotion based on vitals
|
||
let emotion = 'idle';
|
||
let intensity = 0.3;
|
||
|
||
if (energy < 0.3) {
|
||
emotion = this._rand() < 0.5 ? 'nap' : 'content';
|
||
intensity = 0.2;
|
||
} else if (hunger > 0.6) {
|
||
emotion = this._rand() < 0.4 ? 'hungry' : 'idle';
|
||
intensity = 0.4 + hunger * 0.3;
|
||
} else if (loneliness > 0.5) {
|
||
emotion = this._pick(['lonely', 'sad', 'puppy_eyes']);
|
||
// Map puppy_eyes → sad for instrument (no puppy_eyes instrument)
|
||
if (emotion === 'puppy_eyes') emotion = 'sad';
|
||
intensity = 0.3 + loneliness * 0.3;
|
||
} else if (curiosity > 0.6) {
|
||
emotion = this._rand() < 0.5 ? 'curious' : 'alert';
|
||
intensity = 0.3 + curiosity * 0.2;
|
||
} else if (energy > 0.8 && this._rand() < 0.3) {
|
||
emotion = 'playful';
|
||
intensity = 0.4;
|
||
}
|
||
|
||
const babble = this.generate(level, emotion, intensity);
|
||
|
||
// Add idle-specific micro flavor
|
||
const r = this._rand();
|
||
if (r < 0.15 && level >= 3) return this._pick(MICRO_EXPR.yawn) + ' ' + babble;
|
||
if (r < 0.25 && level >= 3) return babble + ' ' + this._pick(MICRO_EXPR.sigh);
|
||
if (r < 0.35 && level >= 5) return this._pick(MICRO_EXPR.sniff) + ' ' + babble + ' ' + this._pick(MICRO_EXPR.sniff);
|
||
return babble;
|
||
}
|
||
|
||
// ── v4: generateAfterSilence — when owner returns ───────────
|
||
generateAfterSilence(level, silenceMinutes, emotion = 'greeting_frenzy') {
|
||
if (silenceMinutes < 5) return this.generate(level, emotion, 0.5);
|
||
|
||
let intensity;
|
||
let em;
|
||
if (silenceMinutes > 120) {
|
||
// Very long absence — explosion of joy
|
||
em = 'greeting_frenzy';
|
||
intensity = 0.95;
|
||
} else if (silenceMinutes > 60) {
|
||
em = 'greeting_frenzy';
|
||
intensity = 0.8;
|
||
} else if (silenceMinutes > 30) {
|
||
em = 'happy';
|
||
intensity = 0.7;
|
||
} else {
|
||
em = 'happy';
|
||
intensity = 0.5;
|
||
}
|
||
|
||
const base = this.generate(level, em, intensity);
|
||
|
||
// Add signature sound for recognition after long absence
|
||
if (silenceMinutes > 60 && level >= 4) {
|
||
return this.signatureSound + '!! ' + base + ' ' + this.signatureSound + '♡';
|
||
}
|
||
return base;
|
||
}
|
||
|
||
// ── generateSemanticReply (with phonetic echo) ─────────────
|
||
generateSemanticReply(level, emotion, intensity, ownerText) {
|
||
const base=this.generate(level,emotion,intensity);
|
||
if (!ownerText||level<2) return base;
|
||
const chance = level<6?0.2 : level<15?0.4 : 0.6;
|
||
if (this._rand()>chance) return base;
|
||
const echo=this.extractPhoneticEcho(ownerText);
|
||
if (!echo) return base;
|
||
const r=this._rand();
|
||
if (r<0.4) return echo+' '+base;
|
||
if (r<0.7) return base+' '+echo;
|
||
const parts=base.split(' ');
|
||
if (parts.length>1) { parts[Math.floor(this._rand()*parts.length)]=echo; return parts.join(' '); }
|
||
return base+' '+echo;
|
||
}
|
||
|
||
// ── extractPhoneticEcho ────────────────────────────────────
|
||
extractPhoneticEcho(ownerText) {
|
||
if (!ownerText||ownerText.length<2) return null;
|
||
const words=ownerText.toLowerCase().replace(/[^а-яёa-z\s]/g,'').split(/\s+/).filter(w=>w.length>=2);
|
||
if (!words.length) return null;
|
||
const word=this._pick(words);
|
||
const frag=word.slice(0,Math.min(3,Math.ceil(word.length*0.4)));
|
||
if (!/[а-яё]/i.test(frag)) return null;
|
||
const hasV=/[аеёиоуыэюя]/i.test(frag);
|
||
let echo = hasV
|
||
? frag+'-'+(frag.match(/[аеёиоуыэюя]/gi)?.pop()||'у').repeat(2)
|
||
: frag+this._pick(this.favV);
|
||
const r=this._rand();
|
||
echo += r<0.3?'~' : r<0.6?'!' : '..';
|
||
return echo;
|
||
}
|
||
|
||
// ── getVocabularyPrompt ────────────────────────────────────
|
||
getVocabularyPrompt(level) {
|
||
const smp=Array.from({length:4},()=>this.generate(level,'idle',0.3+Math.random()*0.4));
|
||
const sig=level>=4?`\nФирменный звук: "${this.signatureSound}"`:'';
|
||
const inv=this.inventedSounds.length?`\nИзобретённые: ${this.inventedSounds.join(', ')}`:'';
|
||
const emo=this.ownerTopEmoji.length?`\nEmoji хозяина: ${this.ownerTopEmoji.join(' ')}`:'';
|
||
const ex=`\nПримеры: ${smp.join(', ')}`;
|
||
|
||
if (level<2) return `\n## Словарь (УР.${level} — новорождённый)\nТолько буквы, шум. ЗАПРЕЩЕНО: "гав","тяф" — не умеет.\nБуквы: ${this.favC.join(',')} + ${this.favV.join(',')}${ex}`;
|
||
if (level<4) return `\n## Словарь (УР.${level} — первые слова)\nКомбинации 2 букв, неуклюже.\nБуквы: ${this.favC.join(',')} + ${this.favV.join(',')}${ex}`;
|
||
if (level<6) return `\n## Словарь (УР.${level} — слоги)\nНастоящие слоги: ${ANIMAL_SOUNDS.tier0.join(', ')}${sig}${ex}`;
|
||
if (level<10) return `\n## Словарь (УР.${level} — щенок)\nЗвуки: ${[...ANIMAL_SOUNDS.tier0,...ANIMAL_SOUNDS.tier1.slice(0,4)].join(', ')}${sig}${emo}\nРитм: Радость=быстро,КАПС | Грусть=тихо,паузы${ex}`;
|
||
if (level<15) return `\n## Словарь (УР.${level} — подросток)\nЗвуки: ${[...ANIMAL_SOUNDS.tier1,...ANIMAL_SOUNDS.tier2].join(', ')}${sig}${inv}${emo}\nЭмоция=инструмент: staccato/bounce/fade/echo${ex}`;
|
||
if (level<20) return `\n## Словарь (УР.${level} — изобретатель)\nЗвуки: ${[...ANIMAL_SOUNDS.tier2,...ANIMAL_SOUNDS.tier3].join(', ')}${sig}${inv}${emo}\nСоздаёт НОВЫЕ звуки. Ответ=музыкальная фраза.${ex}`;
|
||
if (level<40) return `\n## Словарь (УР.${level} — взрослый)\nЗвуки: ${[...ANIMAL_SOUNDS.tier2,...ANIMAL_SOUNDS.tier3].join(', ')}\nСимволы: ${ABSTRACT_SYMBOLS.slice(0,5).join(' ')}${sig}${inv}${emo}\nМастер ритма. Fast+High=барабан, Slow+Low=виолончель${ex}`;
|
||
return `\n## Словарь (УР.${level} — крипто-язык)\nСимволы: ${ABSTRACT_SYMBOLS.join(' ')}\nЗвуки: ${ANIMAL_SOUNDS.tier3.join(', ')}${sig}${inv}${emo}\nКрипто-паттерны: ◈рру◈ ∿ууу∿ ⟡ГАВ!⟡${ex}`;
|
||
}
|
||
|
||
// ── Unlocked sounds ────────────────────────────────────────
|
||
setUnlockedSounds(arr) { this._unlockedSounds = arr||[]; }
|
||
|
||
// ── updateOwnerEmoji (used by telegram.js) ─────────────────
|
||
updateOwnerEmoji(topEmoji) { this.ownerTopEmoji = topEmoji||[]; }
|
||
|
||
// ── v4: Affinity imprint — actually learns from feedback ───
|
||
imprint(lastReply, boost=0.3) {
|
||
if (!lastReply || !this._lastGeneratedAtoms.length) return;
|
||
// Boost affinity for atoms used in the praised reply
|
||
for (const atom of this._lastGeneratedAtoms) {
|
||
if (atom.length < 2) continue; // skip single chars
|
||
const key = atom.toLowerCase().replace(/[^а-яёa-z]/g, '').slice(0, 6);
|
||
if (!key) continue;
|
||
this.affinityMap[key] = Math.min(1.0, (this.affinityMap[key] || 0) + boost);
|
||
}
|
||
// Decay all affinities slightly to prevent lock-in
|
||
for (const k of Object.keys(this.affinityMap)) {
|
||
this.affinityMap[k] *= 0.95;
|
||
if (this.affinityMap[k] < 0.05) delete this.affinityMap[k];
|
||
}
|
||
}
|
||
|
||
// ── generateEnhanced (delegates to generate) ──────────────
|
||
generateEnhanced(level, emotion, intensity, ownerText=null) {
|
||
return ownerText ? this.generateSemanticReply(level,emotion,intensity,ownerText)
|
||
: this.generate(level,emotion,intensity);
|
||
}
|
||
|
||
// ── Serialization ──────────────────────────────────────────
|
||
serialize() {
|
||
return {
|
||
seed:this.seed, soundMemory:this.soundMemory, inventedSounds:this.inventedSounds,
|
||
ownerTopEmoji:this.ownerTopEmoji, favC:this.favC, favV:this.favV,
|
||
signatureSound:this.signatureSound, unlockedSounds:this._unlockedSounds, invCtr:this._invCtr,
|
||
affinityMap:this.affinityMap, conversationMomentum:this._conversationMomentum,
|
||
};
|
||
}
|
||
// Aliases for backward compat (telegram.js uses .save()/.load())
|
||
save() { return this.serialize(); }
|
||
load(data) {
|
||
if (!data) return;
|
||
this.soundMemory=data.soundMemory||[]; this.inventedSounds=data.inventedSounds||[];
|
||
this.ownerTopEmoji=data.ownerTopEmoji||[]; this._invCtr=data.invCtr||data.inventionCounter||0;
|
||
if (data.favC||data.favoriteConsonants) this.favC=data.favC||data.favoriteConsonants;
|
||
if (data.favV||data.favoriteVowels) this.favV=data.favV||data.favoriteVowels;
|
||
if (data.signatureSound) this.signatureSound=data.signatureSound;
|
||
if (data.seed) { this.seed=data.seed; this._rng=this._mkRng(data.seed); }
|
||
if (data.affinityMap) this.affinityMap=data.affinityMap;
|
||
if (data.conversationMomentum) this._conversationMomentum=data.conversationMomentum;
|
||
}
|
||
|
||
static deserialize(data, seed) {
|
||
const e=new BabbleEngine(data?.seed??seed, data?.unlockedSounds??[]);
|
||
if (!data) return e;
|
||
e.load(data);
|
||
return e;
|
||
}
|
||
}
|
||
|
||
export { BabbleEngine, TIER_GATES, ANIMAL_SOUNDS, ABSTRACT_SYMBOLS, EMOTION_INSTRUMENTS, MICRO_EXPR, BREATH_PATTERNS };
|