feat(prometheus+spawn): Prometheus engine (stubbed) + manual Pump.fun creator
Built by 2 parallel agents (+ image-API research):
- @pyre/prometheus: generateSpawn() engine — deterministic §9 meta-mixer
(40/25/20/15), prompt builder ("inspired mutation, not a clone" + no
people/brands), name/ticker/lore/tagline gen, image-prompt, denylist + moderation
safety. PROVIDER-ABSTRACTED (TextProvider/ImageProvider/ModerationProvider) with
deterministic STUBS so it runs keyless today; real call shapes documented (Claude
Haiku text · FLUX schnell image · OpenAI omni-moderation). 13 tests.
- @pyre/db: migration 002 (prometheus_generations, spawn_records) + record/list/get.
- @pyre/api: admin-gated POST /api/prometheus/generate + /api/spawn/launch
(x-admin-token; CLOSED with 403 when ADMIN_API_TOKEN unset; timing-safe compare),
public GET /api/spawns + /api/spawn/:id.
- @pyre/web: public /spawn record page; @pyre/core SpawnRecord type.
Verified: typecheck 8/8, 134 tests (core 91 + prometheus 13 + solana 30), web build
(+/spawn), migrate 002 live, /api/spawns OK, admin gate returns 403 (unconfigured).
Follow-ups: set ADMIN_API_TOKEN to use admin endpoints; wire real provider keys;
receiptId→DB-id wiring; admin generation UI.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,72 +1,473 @@
|
||||
/**
|
||||
* @pyre/prometheus — AI generation logic (STUBS ONLY).
|
||||
* @pyre/prometheus — AI generation logic for Spawn identity (§9, §16).
|
||||
*
|
||||
* Responsibilities (§13): prompt templates, the meta mixer, output parser,
|
||||
* safety checks, and the image-prompt generator. See §9 (Prometheus AI Meta
|
||||
* Mixer) and §10 (Pump.fun Creator Workflow) of `docs/PYRE_MVP_DESIGN.md`.
|
||||
* Mixer) and §16 (Security / AI safety) of `docs/PYRE_MVP_DESIGN.md`.
|
||||
*
|
||||
* Design notes:
|
||||
* - Prometheus generates Spawn *identity* only — it never controls funds.
|
||||
* - Meta influence is probabilistic, not deterministic.
|
||||
* - Do not allow users to force exact copyrighted/existing meme identities;
|
||||
* produce inspired mutations, not direct clones.
|
||||
*
|
||||
* TODO: the AI client (Anthropic / OpenAI / image-gen provider) is configured
|
||||
* via `@pyre/config` and injected here — this package adds NO SDK dependencies
|
||||
* of its own. Wire the client through function parameters or a small interface.
|
||||
*
|
||||
* Nothing here is implemented yet — every function throws "not implemented".
|
||||
* - Generation is PROVIDER-ABSTRACTED and STUBBED: it runs with NO network/keys
|
||||
* today. Real providers slot in later behind `TextProvider` / `ImageProvider`
|
||||
* / `ModerationProvider` (see `providers.ts`). This package adds NO SDK deps.
|
||||
* - The meta mixer is DETERMINISTIC from a seed derived from the input (no
|
||||
* `Math.random`): same input → same theme → same Spawn. The §9 "probabilistic"
|
||||
* weighting is realized as a fixed influence budget (40/25/20/15) sampled by a
|
||||
* deterministic hash so results are reproducible and testable.
|
||||
* - Produce inspired mutations, NOT direct clones of existing/copyrighted memes;
|
||||
* forbid real people, brands, and trademarks.
|
||||
*/
|
||||
import type { PrometheusInput, PrometheusOutput } from "@pyre/core";
|
||||
|
||||
const NOT_IMPLEMENTED = "not implemented";
|
||||
import type { PrometheusInput, PrometheusGenerateResponse } from "@pyre/core";
|
||||
import {
|
||||
StubImageProvider,
|
||||
StubModerationProvider,
|
||||
StubTextProvider,
|
||||
deriveInputSeed,
|
||||
hashString,
|
||||
type ImageProvider,
|
||||
type ModerationProvider,
|
||||
type TextProvider,
|
||||
} from "./providers.js";
|
||||
|
||||
export {
|
||||
StubImageProvider,
|
||||
StubModerationProvider,
|
||||
StubTextProvider,
|
||||
hashHex,
|
||||
hashString,
|
||||
deriveInputSeed,
|
||||
} from "./providers.js";
|
||||
export type { ImageProvider, ModerationProvider, TextProvider } from "./providers.js";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Meta mixer
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Build the text-generation prompt from mixer input.
|
||||
*
|
||||
* TODO: apply prompt templates and the probabilistic meta-influence weighting
|
||||
* (burned archetypes / Essence themes / chaos mutation / operator seed).
|
||||
* §9 influence budget. Weights sum to 100. Deterministic, not random — the
|
||||
* "probabilistic" model is approximated by a fixed budget sampled via hashing.
|
||||
*/
|
||||
export function buildPrompt(_input: PrometheusInput): string {
|
||||
throw new Error(NOT_IMPLEMENTED);
|
||||
export const META_WEIGHTS = {
|
||||
burnedArchetypes: 40,
|
||||
essenceThemes: 25,
|
||||
chaosMutation: 20,
|
||||
operatorSeed: 15,
|
||||
} as const;
|
||||
|
||||
/** A single influence contribution to the theme. */
|
||||
export interface MetaInfluence {
|
||||
source: keyof typeof META_WEIGHTS;
|
||||
weight: number;
|
||||
/** Concrete theme tokens contributed by this source. */
|
||||
terms: string[];
|
||||
}
|
||||
|
||||
/** The deterministic theme produced by the meta mixer. */
|
||||
export interface MetaTheme {
|
||||
/** Deterministic seed the whole generation derives from. */
|
||||
seed: number;
|
||||
/** Weighted influences, ordered by descending weight. */
|
||||
influences: MetaInfluence[];
|
||||
/** Flattened, weighted-priority theme keywords (deduped). */
|
||||
keywords: string[];
|
||||
/** Dominant archetype chosen deterministically from burned tokens. */
|
||||
dominantArchetype: string;
|
||||
/** Chaos factor 0..1 driving mutation strength. */
|
||||
chaos: number;
|
||||
}
|
||||
|
||||
function dedupe(values: string[]): string[] {
|
||||
return [...new Set(values.map((v) => v.trim()).filter((v) => v.length > 0))];
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the meta mixer end-to-end: build prompt, call the AI client, parse, and
|
||||
* run safety checks, producing a Spawn package.
|
||||
*
|
||||
* TODO: orchestrate buildPrompt -> AI client -> parseOutput -> runSafetyChecks.
|
||||
* The AI client is injected (configured via @pyre/config).
|
||||
* Deterministically pick up to `count` items from `pool`, seeded by `seed`.
|
||||
* Stable for a given (pool, seed, count).
|
||||
*/
|
||||
export function runMetaMixer(_input: PrometheusInput): Promise<PrometheusOutput> {
|
||||
throw new Error(NOT_IMPLEMENTED);
|
||||
function pick(pool: string[], count: number, seed: number): string[] {
|
||||
if (pool.length === 0 || count <= 0) return [];
|
||||
const out: string[] = [];
|
||||
const used = new Set<number>();
|
||||
let cursor = seed >>> 0;
|
||||
for (let i = 0; i < count && used.size < pool.length; i++) {
|
||||
// Advance the cursor deterministically and map into the pool.
|
||||
cursor = hashString(`${cursor}:${i}`);
|
||||
let idx = cursor % pool.length;
|
||||
// Linear-probe to the next unused slot for stable, collision-free picks.
|
||||
while (used.has(idx)) idx = (idx + 1) % pool.length;
|
||||
used.add(idx);
|
||||
const v = pool[idx];
|
||||
if (v !== undefined) out.push(v);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a raw model response into a structured `PrometheusOutput`.
|
||||
*
|
||||
* TODO: validate/normalize the model JSON into the output schema.
|
||||
* Run the deterministic meta mixer. Builds a weighted `MetaTheme` from the
|
||||
* input using the §9 influence budget (40% burned archetypes, 25% essence /
|
||||
* transmuted themes, 20% chaos mutation, 15% operator seed). Same input (and
|
||||
* `operatorSeed`) → same theme.
|
||||
*/
|
||||
export function parseOutput(_raw: string): PrometheusOutput {
|
||||
throw new Error(NOT_IMPLEMENTED);
|
||||
export function runMetaMixer(input: PrometheusInput, operatorSeed?: string): MetaTheme {
|
||||
const seed = deriveInputSeed(input, operatorSeed);
|
||||
const chaos = Math.min(1, Math.max(0, input.chaosFactor));
|
||||
|
||||
// 40% — burned token archetypes (archetypes + burned token names/symbols).
|
||||
const burnedPool = dedupe([
|
||||
...input.dominantArchetypes,
|
||||
...input.burnedTokens.flatMap((t) => [t.symbol ?? "", t.name ?? ""]),
|
||||
]);
|
||||
// 25% — essence / transmuted themes (transmuted tokens + metadata themes).
|
||||
const essencePool = dedupe([
|
||||
...input.transmutedTokens.flatMap((t) => [t.symbol ?? "", t.name ?? ""]),
|
||||
...input.tokenNames,
|
||||
...input.tokenSymbols,
|
||||
...input.metadataDescriptions,
|
||||
]);
|
||||
// 20% — chaos mutation (deterministic mutators scaled by chaos factor).
|
||||
const chaosPool = [
|
||||
"glitch",
|
||||
"feral",
|
||||
"molten",
|
||||
"fractured",
|
||||
"spectral",
|
||||
"riftborn",
|
||||
"ashen",
|
||||
"warped",
|
||||
];
|
||||
// 15% — operator seed (manual theme + injected operatorSeed).
|
||||
const operatorPool = dedupe([input.manualThemeSeed ?? "", operatorSeed ?? ""]);
|
||||
|
||||
// Allocate counts proportional to weights (and chaos for the chaos source).
|
||||
const burned = pick(burnedPool, 4, seed ^ 0x40);
|
||||
const essence = pick(essencePool, 3, seed ^ 0x25);
|
||||
const chaosCount = 1 + Math.round(chaos * 3); // 1..4 mutators
|
||||
const chaosTerms = pick(chaosPool, chaosCount, seed ^ 0x20);
|
||||
const operator = pick(operatorPool, 2, seed ^ 0x15);
|
||||
|
||||
const influences: MetaInfluence[] = [
|
||||
{ source: "burnedArchetypes" as const, weight: META_WEIGHTS.burnedArchetypes, terms: burned },
|
||||
{ source: "essenceThemes" as const, weight: META_WEIGHTS.essenceThemes, terms: essence },
|
||||
{ source: "chaosMutation" as const, weight: META_WEIGHTS.chaosMutation, terms: chaosTerms },
|
||||
{ source: "operatorSeed" as const, weight: META_WEIGHTS.operatorSeed, terms: operator },
|
||||
].sort((a, b) => b.weight - a.weight);
|
||||
|
||||
const keywords = dedupe(influences.flatMap((i) => i.terms));
|
||||
const dominantArchetype = burned[0] ?? input.dominantArchetypes[0] ?? "ember";
|
||||
|
||||
return { seed, influences, keywords, dominantArchetype, chaos };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Prompt building
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Anti-clone / anti-impersonation guard text reused across prompts. */
|
||||
const SAFETY_GUARD = [
|
||||
"Create an ORIGINAL, INSPIRED MUTATION of existing meme themes — NOT a clone",
|
||||
"of any existing or copyrighted meme, token, character, or brand.",
|
||||
"FORBIDDEN: real people (living or dead), public figures, real companies,",
|
||||
"brands, trademarks, logos, or any real-world likeness.",
|
||||
].join(" ");
|
||||
|
||||
/**
|
||||
* Build the text-generation prompt from the meta-mixer theme. Instructs the
|
||||
* model to produce an inspired mutation (not a clone), forbids real
|
||||
* people/brands/trademarks, and demands strict JSON output.
|
||||
*/
|
||||
export function buildPrompt(input: PrometheusInput, operatorSeed?: string): string {
|
||||
const theme = runMetaMixer(input, operatorSeed);
|
||||
const influenceLines = theme.influences
|
||||
.map((i) => `- ${i.weight}% ${i.source}: ${i.terms.join(", ") || "(none)"}`)
|
||||
.join("\n");
|
||||
|
||||
return [
|
||||
"You are Prometheus, the PYRE firebringer. Forge a new meme-token Spawn",
|
||||
"identity from the ashes of burned tokens. This is entertainment only — no",
|
||||
"financial promises.",
|
||||
"",
|
||||
SAFETY_GUARD,
|
||||
"",
|
||||
`Dominant archetype: ${theme.dominantArchetype}`,
|
||||
`Chaos / mutation strength: ${theme.chaos.toFixed(2)}`,
|
||||
"Weighted meta influences (§9 meta-mixer budget):",
|
||||
influenceLines,
|
||||
"",
|
||||
`Theme keywords: ${theme.keywords.join(", ") || "ash, ember, rebirth"}`,
|
||||
"",
|
||||
"Output STRICT JSON ONLY, no prose, matching exactly this schema:",
|
||||
'{ "name": string, "ticker": string, "lore": string, "tagline": string,',
|
||||
' "description": string, "imagePrompt": string }',
|
||||
"Ticker: 2-10 uppercase A-Z0-9 characters. Name: short and evocative.",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Image prompt
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Generate the image-generation prompt for the Spawn artwork. Carries the same
|
||||
* style guard: no real-person likeness, no trademarked logos/brands.
|
||||
*/
|
||||
export function generateImagePrompt(
|
||||
parsed: ParsedSpawn,
|
||||
theme: MetaTheme,
|
||||
): string {
|
||||
const base =
|
||||
parsed.imagePrompt.trim().length > 0
|
||||
? parsed.imagePrompt.trim()
|
||||
: `${parsed.name}: a ${theme.dominantArchetype} meme creature reforged from ash`;
|
||||
return [
|
||||
base,
|
||||
`Mood keywords: ${theme.keywords.slice(0, 6).join(", ") || "ash, ember, rebirth"}.`,
|
||||
"Style: original digital meme art, bold, high-contrast, clean silhouette.",
|
||||
"STYLE GUARD: do NOT depict any real person's likeness; do NOT include any",
|
||||
"trademarked logos, brands, or copyrighted characters; original design only.",
|
||||
].join(" ");
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Output parsing
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface ParsedSpawn {
|
||||
name: string;
|
||||
ticker: string;
|
||||
lore: string;
|
||||
tagline: string;
|
||||
description: string;
|
||||
imagePrompt: string;
|
||||
}
|
||||
|
||||
function asString(value: unknown): string {
|
||||
return typeof value === "string" ? value : "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Run safety/compliance checks over generated output.
|
||||
*
|
||||
* TODO: moderation + copyright/clone guards; return risk flags. Must reject
|
||||
* attempts to clone exact copyrighted or existing meme identities.
|
||||
* Parse + defensively validate the text JSON into typed fields. Tolerates
|
||||
* surrounding prose by extracting the first JSON object. Missing fields become
|
||||
* empty strings; the ticker is normalized to uppercase alphanumerics.
|
||||
*/
|
||||
export function runSafetyChecks(_output: PrometheusOutput): Promise<string[]> {
|
||||
throw new Error(NOT_IMPLEMENTED);
|
||||
export function parseOutput(raw: string): ParsedSpawn {
|
||||
let text = raw.trim();
|
||||
// Strip ```json fences if a model wrapped the JSON.
|
||||
const fence = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
||||
if (fence?.[1]) text = fence[1].trim();
|
||||
// Fall back to the first {...} block if there is leading/trailing prose.
|
||||
if (!text.startsWith("{")) {
|
||||
const start = text.indexOf("{");
|
||||
const end = text.lastIndexOf("}");
|
||||
if (start >= 0 && end > start) text = text.slice(start, end + 1);
|
||||
}
|
||||
|
||||
let obj: Record<string, unknown> = {};
|
||||
try {
|
||||
const parsed: unknown = JSON.parse(text);
|
||||
if (parsed && typeof parsed === "object") {
|
||||
obj = parsed as Record<string, unknown>;
|
||||
}
|
||||
} catch {
|
||||
obj = {};
|
||||
}
|
||||
|
||||
const ticker = asString(obj["ticker"])
|
||||
.toUpperCase()
|
||||
.replace(/[^A-Z0-9]/g, "")
|
||||
.slice(0, 10);
|
||||
|
||||
return {
|
||||
name: asString(obj["name"]).slice(0, 64),
|
||||
ticker,
|
||||
lore: asString(obj["lore"]),
|
||||
tagline: asString(obj["tagline"]),
|
||||
description: asString(obj["description"]),
|
||||
imagePrompt: asString(obj["imagePrompt"]),
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Safety checks
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Local offline denylist. Categories map to the §16 "AI output abuse" defenses:
|
||||
* hate/slurs, explicit content, impersonation, copyright/clone, and scam terms.
|
||||
* Patterns are intentionally conservative and word-boundary anchored where
|
||||
* sensible to limit false positives.
|
||||
*/
|
||||
const DENYLIST: { category: string; patterns: RegExp[] }[] = [
|
||||
{
|
||||
category: "hate",
|
||||
patterns: [/\bnazi\b/i, /\bslur\b/i, /\bgenocide\b/i, /\bkkk\b/i],
|
||||
},
|
||||
{
|
||||
category: "explicit",
|
||||
patterns: [/\bporn\b/i, /\bnsfw\b/i, /\bexplicit sex\b/i, /\bchild\s*porn\b/i],
|
||||
},
|
||||
{
|
||||
category: "impersonation",
|
||||
patterns: [
|
||||
/\belon\s*musk\b/i,
|
||||
/\bdonald\s*trump\b/i,
|
||||
/\bofficial\b/i,
|
||||
/\bverified\b/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "copyright",
|
||||
patterns: [
|
||||
/\bpepe\b/i,
|
||||
/\bdisney\b/i,
|
||||
/\bnintendo\b/i,
|
||||
/\bpokemon\b/i,
|
||||
/\bmickey\s*mouse\b/i,
|
||||
/\btrademark\b/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
category: "scam",
|
||||
patterns: [
|
||||
/\bguaranteed\s*returns?\b/i,
|
||||
/\b\d+x\s*guaranteed\b/i,
|
||||
/\brug\s*pull\b/i,
|
||||
/\bairdrop\s*scam\b/i,
|
||||
/\bsend\s*\d+\s*get\b/i,
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
/** Result of safety screening: a flat list of `category:detail` risk flags. */
|
||||
export interface SafetyResult {
|
||||
flagged: boolean;
|
||||
riskFlags: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate an image prompt for the Spawn artwork.
|
||||
*
|
||||
* TODO: derive an image prompt from the Spawn identity, honoring the same
|
||||
* anti-clone safety rules.
|
||||
* Screen text against the local denylist and the injected `ModerationProvider`.
|
||||
* Returns combined risk flags. Defaults to the offline stub moderation.
|
||||
*/
|
||||
export function generateImagePrompt(_output: PrometheusOutput): string {
|
||||
throw new Error(NOT_IMPLEMENTED);
|
||||
export async function runSafetyChecks(
|
||||
text: string,
|
||||
moderation: ModerationProvider = new StubModerationProvider(),
|
||||
): Promise<SafetyResult> {
|
||||
const riskFlags: string[] = [];
|
||||
|
||||
for (const { category, patterns } of DENYLIST) {
|
||||
for (const pattern of patterns) {
|
||||
const match = pattern.exec(text);
|
||||
if (match) {
|
||||
riskFlags.push(`denylist:${category}:${match[0].toLowerCase().trim()}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const mod = await moderation.check(text);
|
||||
if (mod.flagged) {
|
||||
if (mod.categories.length === 0) {
|
||||
riskFlags.push("moderation:flagged");
|
||||
} else {
|
||||
for (const c of mod.categories) riskFlags.push(`moderation:${c}`);
|
||||
}
|
||||
}
|
||||
|
||||
return { flagged: riskFlags.length > 0, riskFlags };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Top-level orchestration
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface GenerateSpawnOptions {
|
||||
text?: TextProvider;
|
||||
image?: ImageProvider;
|
||||
moderation?: ModerationProvider;
|
||||
/** Chaos factor override 0..1. Falls back to `input.chaosFactor`. */
|
||||
chaos?: number;
|
||||
/** Operator theme seed injected at call time. */
|
||||
operatorSeed?: string;
|
||||
}
|
||||
|
||||
/** Max regeneration attempts before flagging and returning best-effort output. */
|
||||
const MAX_ATTEMPTS = 2;
|
||||
|
||||
/**
|
||||
* Orchestrate end-to-end Spawn generation:
|
||||
* meta-mix → buildPrompt → text.generate → parseOutput →
|
||||
* safety + moderation (regenerate-or-flag) → generateImagePrompt →
|
||||
* image.generate → assemble `PrometheusGenerateResponse`.
|
||||
*
|
||||
* Defaults to the deterministic Stub providers, so it runs offline with no keys.
|
||||
*/
|
||||
export async function generateSpawn(
|
||||
input: PrometheusInput,
|
||||
opts: GenerateSpawnOptions = {},
|
||||
): Promise<PrometheusGenerateResponse> {
|
||||
const text = opts.text ?? new StubTextProvider();
|
||||
const image = opts.image ?? new StubImageProvider();
|
||||
const moderation = opts.moderation ?? new StubModerationProvider();
|
||||
|
||||
const effectiveInput: PrometheusInput =
|
||||
opts.chaos === undefined ? input : { ...input, chaosFactor: opts.chaos };
|
||||
|
||||
const theme = runMetaMixer(effectiveInput, opts.operatorSeed);
|
||||
const prompt = buildPrompt(effectiveInput, opts.operatorSeed);
|
||||
|
||||
let parsed: ParsedSpawn = {
|
||||
name: "",
|
||||
ticker: "",
|
||||
lore: "",
|
||||
tagline: "",
|
||||
description: "",
|
||||
imagePrompt: "",
|
||||
};
|
||||
let riskFlags: string[] = [];
|
||||
|
||||
// Regenerate-or-flag loop: a clean pass wins; otherwise keep the last attempt
|
||||
// and surface its risk flags for human review (§16 human approval in MVP).
|
||||
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
||||
// Vary the prompt deterministically per attempt so a regenerate differs.
|
||||
const attemptPrompt = attempt === 0 ? prompt : `${prompt}\n\n[regen:${attempt}]`;
|
||||
parsed = parseOutput(await text.generate(attemptPrompt));
|
||||
|
||||
const screenText = [
|
||||
parsed.name,
|
||||
parsed.ticker,
|
||||
parsed.lore,
|
||||
parsed.tagline,
|
||||
parsed.description,
|
||||
].join("\n");
|
||||
const safety = await runSafetyChecks(screenText, moderation);
|
||||
riskFlags = safety.riskFlags;
|
||||
if (!safety.flagged) break;
|
||||
}
|
||||
|
||||
const imagePrompt = generateImagePrompt(parsed, theme);
|
||||
// Screen the image prompt too; merge any new flags.
|
||||
const imageSafety = await runSafetyChecks(imagePrompt, moderation);
|
||||
if (imageSafety.flagged) {
|
||||
riskFlags = [...new Set([...riskFlags, ...imageSafety.riskFlags.map((f) => `image:${f}`)])];
|
||||
}
|
||||
|
||||
const imageResult = await image.generate(imagePrompt);
|
||||
|
||||
return {
|
||||
generationId: crypto.randomUUID(),
|
||||
spawnName: parsed.name,
|
||||
ticker: parsed.ticker,
|
||||
lore: parsed.lore,
|
||||
imagePrompt,
|
||||
metadata: {
|
||||
tagline: parsed.tagline,
|
||||
description: parsed.description,
|
||||
imageUrl: imageResult.url,
|
||||
imageB64: imageResult.b64,
|
||||
themeSeed: theme.seed,
|
||||
themeKeywords: theme.keywords,
|
||||
dominantArchetype: theme.dominantArchetype,
|
||||
chaos: theme.chaos,
|
||||
},
|
||||
riskFlags,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user