feat: content generation engine - spintax resolver, API endpoints, BullMQ worker
This commit is contained in:
154
src/lib/spintax/resolver.ts
Normal file
154
src/lib/spintax/resolver.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
// Spintax Resolver - Handles {A|B|C} syntax
|
||||
import crypto from 'crypto';
|
||||
|
||||
export interface SpintaxChoice {
|
||||
path: string;
|
||||
chosen: string;
|
||||
allOptions: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves spintax syntax {A|B|C} to a single choice
|
||||
* Tracks which choices were made for uniqueness
|
||||
*/
|
||||
export class SpintaxResolver {
|
||||
private choices: SpintaxChoice[] = [];
|
||||
private seed: string;
|
||||
|
||||
constructor(seed?: string) {
|
||||
this.seed = seed || crypto.randomBytes(16).toString('hex');
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve all spintax in text
|
||||
*/
|
||||
resolve(text: string): string {
|
||||
let resolved = text;
|
||||
let iteration = 0;
|
||||
|
||||
// Handle nested spintax with multiple passes
|
||||
while (resolved.includes('{') && resolved.includes('}') && iteration < 10) {
|
||||
resolved = this.resolvePass(resolved, iteration);
|
||||
iteration++;
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
private resolvePass(text: string, iteration: number): string {
|
||||
const regex = /\{([^{}]+)\}/g;
|
||||
let result = text;
|
||||
let match;
|
||||
let offset = 0;
|
||||
|
||||
while ((match = regex.exec(text)) !== null) {
|
||||
const fullMatch = match[0];
|
||||
const options = match[1].split('|');
|
||||
|
||||
// Deterministic choice based on seed + position
|
||||
const choiceIndex = this.getChoiceIndex(match.index + iteration, options.length);
|
||||
const chosen = options[choiceIndex].trim();
|
||||
|
||||
// Track the choice
|
||||
this.choices.push({
|
||||
path: `pos_${match.index}_iter_${iteration}`,
|
||||
chosen,
|
||||
allOptions: options
|
||||
});
|
||||
|
||||
// Replace in result
|
||||
const beforeMatch = result.substring(0, match.index + offset);
|
||||
const afterMatch = result.substring(match.index + offset + fullMatch.length);
|
||||
result = beforeMatch + chosen + afterMatch;
|
||||
|
||||
offset += chosen.length - fullMatch.length;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private getChoiceIndex(position: number, optionsCount: number): number {
|
||||
const hash = crypto.createHash('sha256')
|
||||
.update(`${this.seed}_${position}`)
|
||||
.digest('hex');
|
||||
return parseInt(hash.substring(0, 8), 16) % optionsCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all choices made during resolution
|
||||
*/
|
||||
getChoices(): SpintaxChoice[] {
|
||||
return this.choices;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate hash of choices for uniqueness checking
|
||||
*/
|
||||
getChoicesHash(): string {
|
||||
const choiceString = this.choices
|
||||
.map(c => c.chosen)
|
||||
.join('::');
|
||||
return crypto.createHash('sha256')
|
||||
.update(choiceString)
|
||||
.digest('hex')
|
||||
.substring(0, 32);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset for new resolution
|
||||
*/
|
||||
reset(newSeed?: string) {
|
||||
this.choices = [];
|
||||
if (newSeed) this.seed = newSeed;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Expand variables like {{CITY}} in text
|
||||
*/
|
||||
export function expandVariables(text: string, variables: Record<string, string>): string {
|
||||
let result = text;
|
||||
|
||||
for (const [key, value] of Object.entries(variables)) {
|
||||
const regex = new RegExp(`\\{\\{${key}\\}\\}`, 'g');
|
||||
result = result.replace(regex, value);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate cartesian product of pipe-separated values
|
||||
* Example: { CITY: "A|B", STATE: "X|Y" } => 4 combinations
|
||||
*/
|
||||
export function generateCartesianProduct(
|
||||
variables: Record<string, string>
|
||||
): Array<Record<string, string>> {
|
||||
const keys = Object.keys(variables);
|
||||
const values = keys.map(key => variables[key].split('|').map(v => v.trim()));
|
||||
|
||||
function* cartesian(arrays: string[][]): Generator<string[]> {
|
||||
if (arrays.length === 0) {
|
||||
yield [];
|
||||
return;
|
||||
}
|
||||
|
||||
const [first, ...rest] = arrays;
|
||||
for (const value of first) {
|
||||
for (const combo of cartesian(rest)) {
|
||||
yield [value, ...combo];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const results: Array<Record<string, string>> = [];
|
||||
for (const combo of cartesian(values)) {
|
||||
const obj: Record<string, string> = {};
|
||||
keys.forEach((key, i) => {
|
||||
obj[key] = combo[i];
|
||||
});
|
||||
results.push(obj);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
Reference in New Issue
Block a user