Monorepo for Aesthetic.Computer aesthetic.computer
at main 236 lines 9.2 kB view raw
1// Generate Short Code, 25.10.22 2// Shared code generator for paintings, tapes, and kidlisp 3// Supports both random and inferred (source-aware) code generation 4 5import { customAlphabet } from 'nanoid'; 6 7// Code generator alphabets - try lowercase first, add uppercase only on collision 8const lowercaseConsonants = 'bcdfghjklmnpqrstvwxyz'; 9const lowercaseVowels = 'aeiou'; 10const uppercaseConsonants = 'BCDFGHJKLMNPQRSTVWXYZ'; 11const uppercaseVowels = 'AEIOU'; 12const numbers = '23456789'; // Exclude 0,1 (look like O,l) 13 14// Lowercase-only alphabet for initial attempts 15const lowercaseAlphabet = lowercaseConsonants + lowercaseVowels + numbers; 16// Full alphabet with uppercase for collisions 17const fullAlphabet = lowercaseConsonants + lowercaseVowels + uppercaseConsonants + uppercaseVowels + numbers; 18 19const CODE_LENGTH = 3; 20const LOWERCASE_ATTEMPTS = 50; // Try lowercase-only first 21const MAX_COLLISION_ATTEMPTS = 100; 22 23const lowercaseNanoid = customAlphabet(lowercaseAlphabet, CODE_LENGTH); 24const fullNanoid = customAlphabet(fullAlphabet, CODE_LENGTH); 25 26/** 27 * Generate a unique short code with MongoDB collision checking 28 * @param {Object} collection - MongoDB collection to check for existing codes 29 * @param {Object} options - Generation options 30 * @param {string} options.mode - 'random' or 'inferred' (default: 'random') 31 * @param {string} options.sourceText - Source text for inference (required if mode='inferred') 32 * @param {string} options.type - Content type: 'kidlisp', 'painting', or 'tape' 33 * @returns {Promise<string>} Unique 3-character code (or longer if exhausted) 34 */ 35export async function generateUniqueCode(collection, options = {}) { 36 const { mode = 'random', sourceText, type } = options; 37 38 // For inferred mode, try intelligent codes first 39 if (mode === 'inferred' && sourceText && type === 'kidlisp') { 40 console.log(`🧠 Attempting inferred code generation from source...`); 41 const inferredCodes = await generateInferredCodes(sourceText, type); 42 43 // Try each inferred code 44 for (const inferredCode of inferredCodes) { 45 const existing = await collection.findOne({ code: inferredCode }); 46 if (!existing) { 47 console.log(`✨ Using inferred code: ${inferredCode}`); 48 return inferredCode; 49 } 50 } 51 52 console.log(`⚠️ All inferred codes taken, falling back to random generation`); 53 } 54 55 // Random generation (or fallback from inferred) 56 return await generateRandomCode(collection); 57} 58 59/** 60 * Generate random pronounceable code with collision checking 61 * @param {Object} collection - MongoDB collection to check for existing codes 62 * @returns {Promise<string>} Unique code 63 */ 64async function generateRandomCode(collection) { 65 // First 50 attempts: lowercase only 66 for (let attempt = 0; attempt < LOWERCASE_ATTEMPTS; attempt++) { 67 const code = lowercaseNanoid(); 68 69 const existing = await collection.findOne({ code }); 70 if (!existing) { 71 return code; 72 } 73 74 console.log(`⚠️ Lowercase code collision detected: ${code}, retrying...`); 75 } 76 77 // Next 50 attempts: include uppercase 78 for (let attempt = LOWERCASE_ATTEMPTS; attempt < MAX_COLLISION_ATTEMPTS; attempt++) { 79 const code = fullNanoid(); 80 81 const existing = await collection.findOne({ code }); 82 if (!existing) { 83 console.log(`⚠️ Using uppercase in code after ${LOWERCASE_ATTEMPTS} lowercase attempts: ${code}`); 84 return code; 85 } 86 87 console.log(`⚠️ Full alphabet code collision detected: ${code}, retrying...`); 88 } 89 90 // If we hit max attempts, use a longer code with full alphabet 91 const longerNanoid = customAlphabet(fullAlphabet, CODE_LENGTH + 1); 92 const longerCode = longerNanoid(); 93 console.log(`⚠️ Max collisions reached, using longer code: ${longerCode}`); 94 return longerCode; 95} 96 97/** 98 * Generate inferred codes from source text (KidLisp source code) 99 * Analyzes text to create meaningful, pronounceable codes 100 * @param {string} sourceText - Source code to analyze 101 * @param {string} type - Content type (currently only 'kidlisp' supported) 102 * @returns {Promise<string[]>} Array of candidate codes, sorted by quality 103 */ 104async function generateInferredCodes(sourceText, type) { 105 const codes = []; 106 const cleanSource = sourceText.trim().toLowerCase(); 107 108 // Temporal hints (day of week, date markers) 109 const now = new Date(); 110 const dayOfWeek = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'][now.getDay()]; 111 const dayChar = dayOfWeek.charAt(0); // 's', 'm', 't', 'w', etc. 112 const monthChar = ['j', 'f', 'm', 'a', 'm', 'j', 'j', 'a', 's', 'o', 'n', 'd'][now.getMonth()]; 113 const dayOfMonth = now.getDate(); 114 const isWeekend = now.getDay() === 0 || now.getDay() === 6; 115 116 // Phonemic helpers for better human readability 117 const vowels = 'aeiou'; 118 const consonants = 'bcdfghjklmnpqrstvwxyz'; 119 120 // Check if a string has good vowel-consonant balance for pronunciation 121 function hasGoodPhonetics(str) { 122 const hasVowel = /[aeiou]/.test(str); 123 const hasConsonant = /[bcdfghjklmnpqrstvwxyz]/.test(str); 124 return hasVowel && hasConsonant; 125 } 126 127 // Create pronounceable combinations by inserting vowels 128 function makePronounceable(consonantString, targetLength = 3) { 129 if (consonantString.length === 0) return ''; 130 if (consonantString.length >= targetLength && hasGoodPhonetics(consonantString)) { 131 return consonantString.substring(0, targetLength); 132 } 133 134 let result = consonantString.charAt(0); 135 const vowelChoices = ['a', 'e', 'i', 'o', 'u']; 136 137 for (let i = 1; i < consonantString.length && result.length < targetLength; i++) { 138 if (result.length < targetLength - 1) { 139 result += vowelChoices[i % vowelChoices.length]; 140 } 141 if (result.length < targetLength) { 142 result += consonantString.charAt(i); 143 } 144 } 145 146 while (result.length < targetLength) { 147 result += vowelChoices[result.length % vowelChoices.length]; 148 } 149 150 return result.substring(0, targetLength); 151 } 152 153 // KidLisp-specific inference 154 if (type === 'kidlisp') { 155 // Common KidLisp functions 156 const kidlispFunctions = [ 157 'wipe', 'ink', 'line', 'box', 'circle', 'rect', 'def', 'later', 158 'scroll', 'resolution', 'gap', 'frame', 'brush', 'clear', 'repeat' 159 ]; 160 161 const colors = ['red', 'blue', 'green', 'yellow', 'white', 'black', 'gray', 'purple', 'orange']; 162 163 // Find functions in source 164 const foundFunctions = kidlispFunctions.filter(fn => 165 cleanSource.includes(`(${fn}`) || cleanSource.includes(` ${fn} `) 166 ); 167 168 const foundColors = colors.filter(color => cleanSource.includes(color)); 169 170 // Strategy 1: First letters of functions with vowel insertion 171 if (foundFunctions.length >= 2) { 172 const firstLetters = foundFunctions.slice(0, 3).map(fn => fn.charAt(0)).join(''); 173 codes.push(makePronounceable(firstLetters, 3)); 174 codes.push(makePronounceable(firstLetters, 4)); 175 } 176 177 // Strategy 2: Function + color combinations 178 if (foundFunctions.length > 0 && foundColors.length > 0) { 179 const f = foundFunctions[0].charAt(0); 180 const c = foundColors[0].charAt(0); 181 codes.push(f + 'a' + c); // "wab" for wipe blue 182 codes.push(f + 'i' + c); // "wib" 183 codes.push(f + 'o' + c); // "wob" 184 } 185 186 // Strategy 3: Line-by-line first letters 187 const lines = cleanSource.split('\n').map(l => l.trim()).filter(l => l.length > 0); 188 if (lines.length >= 2 && lines.length <= 6) { 189 const lineWords = lines.map(line => (line.match(/[a-z]+/g) || [])[0]).filter(w => w); 190 if (lineWords.length >= 2) { 191 const lineCode = lineWords.map(w => w.charAt(0)).join(''); 192 codes.push(makePronounceable(lineCode, 3)); 193 } 194 } 195 196 // Strategy 4: Extract numbers 197 const numbers = cleanSource.match(/\d+/g) || []; 198 if (numbers.length > 0 && foundFunctions.length > 0) { 199 const digit = numbers[0].charAt(0); 200 const f = foundFunctions[0].charAt(0); 201 codes.push(f + digit + (foundFunctions[1] ? foundFunctions[1].charAt(0) : 'a')); 202 } 203 204 // Temporal hints - VERY sparingly, only 2 codes, lowest priority 205 // These appear after all meaningful strategies as a last resort 206 if (foundFunctions.length > 0) { 207 const f = foundFunctions[0].charAt(0); 208 codes.push(f + dayChar + 'a'); // e.g., "wwa" for wipe-Wednesday 209 if (dayOfMonth < 10) { 210 codes.push(f + dayOfMonth + 'a'); // e.g., "w2a" for wipe on 2nd 211 } 212 } 213 } 214 215 // Filter and score codes 216 const validCodes = codes.filter(code => 217 /^[a-z0-9]+$/.test(code) && 218 code.length >= 3 && 219 code.length <= 4 && 220 /[a-z]/.test(code) 221 ); 222 223 // Score codes by phonetic quality 224 const scorePhonetics = (code) => { 225 let score = 0; 226 if (hasGoodPhonetics(code)) score += 10; 227 if (/^[bcdfghjklmnpqrstvwxyz][aeiou]/.test(code)) score += 5; 228 if (/[aeiou][bcdfghjklmnpqrstvwxyz]$/.test(code)) score += 3; 229 if (code.length === 3) score += 2; 230 return score; 231 }; 232 233 return [...new Set(validCodes)] 234 .sort((a, b) => scorePhonetics(b) - scorePhonetics(a)) 235 .slice(0, 20); // Return top 20 candidates 236}