@mcptoolshop/style-dataset-lab 3.0.1 → 3.2.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/CHANGELOG.md +77 -0
- package/README.es.md +154 -41
- package/README.fr.md +153 -40
- package/README.hi.md +160 -47
- package/README.it.md +155 -42
- package/README.ja.md +159 -46
- package/README.md +40 -0
- package/README.pt-BR.md +154 -41
- package/README.zh.md +154 -41
- package/bin/sdlab.js +81 -2
- package/lib/adapters/ai-toolkit.js +209 -0
- package/lib/adapters/comfyui-runner.js +11 -15
- package/lib/adapters/comfyui-workflows.js +133 -2
- package/lib/adapters/diffusers-lora.js +46 -66
- package/lib/adapters/generic-image-caption.js +22 -50
- package/lib/batch-runs.js +62 -0
- package/lib/canon-build/build.js +450 -0
- package/lib/canon-build/emitters/base.js +122 -0
- package/lib/canon-build/emitters/character.js +167 -0
- package/lib/canon-build/emitters/deity.js +165 -0
- package/lib/canon-build/emitters/faction.js +161 -0
- package/lib/canon-build/emitters/index.js +38 -0
- package/lib/canon-build/emitters/location.js +153 -0
- package/lib/canon-build/emitters/monster.js +131 -0
- package/lib/canon-build/emitters/relic.js +142 -0
- package/lib/canon-build/emitters/ship.js +154 -0
- package/lib/canon-build/emitters/species.js +165 -0
- package/lib/canon-build/frontmatter.js +76 -0
- package/lib/canon-build/lane-resolve.js +64 -0
- package/lib/canon-build/load-config.js +117 -0
- package/lib/canon-build/load-entry.js +62 -0
- package/lib/canon-build/load-schema.js +46 -0
- package/lib/captions.js +197 -0
- package/lib/comfyui-output.js +70 -0
- package/lib/config.js +19 -0
- package/lib/freeze-events.js +109 -0
- package/lib/freeze-gate.js +165 -0
- package/lib/freeze-stamp.js +161 -0
- package/lib/rows.js +228 -0
- package/lib/training-adapters.js +84 -0
- package/lib/training-packages.js +25 -32
- package/lib/training-profiles.js +43 -0
- package/package.json +29 -2
- package/scripts/_batch_cut.py +71 -0
- package/scripts/_seam_cutout.py +74 -0
- package/scripts/batch-generate.js +56 -13
- package/scripts/canon-build.js +88 -0
- package/scripts/canon-drift.js +193 -0
- package/scripts/canon-freeze-status.js +85 -0
- package/scripts/canon-freeze.js +211 -0
- package/scripts/canon-unfreeze.js +137 -0
- package/scripts/curate.js +12 -0
- package/scripts/generate-controlnet.js +12 -0
- package/scripts/generate-identity.js +20 -4
- package/scripts/generate-ipadapter.js +11 -0
- package/scripts/generate.js +250 -102
- package/scripts/qwen_generate.py +124 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Character emitter (D5: character.schema.json).
|
|
3
|
+
*
|
|
4
|
+
* Caption: trigger + " style, a " + kind + " wearing " + visual.attire + ", " + visual.silhouette_cue + ". " + visual.build + " build, " + visual.hair
|
|
5
|
+
* Prompt positive: visual.* (silhouette_cue, signature_prop, distinguishing_marks, build, hair, eyes, age_band, attire, palette) + signature_features + pose hint from narrative.voice
|
|
6
|
+
* Prompt negative: forbidden_inputs + divine-radiance suppression when kind="mortal"
|
|
7
|
+
* Context sections: Identity, Role & Voice, Relationships, Canonical Events, Mechanical (optional), Forbidden, Sources
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { contextFrontmatter, renderKeyValueSection, renderSection } from './base.js';
|
|
11
|
+
|
|
12
|
+
const SCHEMA_KIND = 'character';
|
|
13
|
+
|
|
14
|
+
export function emitCharacter(entry, ctx) {
|
|
15
|
+
const fm = entry.frontmatter;
|
|
16
|
+
return {
|
|
17
|
+
schemaKind: SCHEMA_KIND,
|
|
18
|
+
caption: buildCaption(fm, ctx),
|
|
19
|
+
prompt: buildPromptTemplate(fm),
|
|
20
|
+
context: buildContext(entry, ctx),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function buildCaption(fm, ctx) {
|
|
25
|
+
const v = fm.visual || {};
|
|
26
|
+
const segments = [];
|
|
27
|
+
if (ctx.trigger) segments.push(`${ctx.trigger} style`);
|
|
28
|
+
|
|
29
|
+
const kind = fm.kind || fm.role_class || 'figure';
|
|
30
|
+
const attire = v.attire || '';
|
|
31
|
+
const silhouette = v.silhouette_cue || '';
|
|
32
|
+
const subjectClause = attire ? `a ${kind} wearing ${attire}` : `a ${kind}`;
|
|
33
|
+
segments.push(silhouette ? `${subjectClause}, ${silhouette}` : subjectClause);
|
|
34
|
+
|
|
35
|
+
const buildAndHair = [];
|
|
36
|
+
if (v.build) buildAndHair.push(`${v.build} build`);
|
|
37
|
+
if (v.hair) buildAndHair.push(v.hair);
|
|
38
|
+
if (buildAndHair.length) segments.push(buildAndHair.join(', '));
|
|
39
|
+
|
|
40
|
+
return segments.join(', ');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function buildPromptTemplate(fm) {
|
|
44
|
+
const v = fm.visual || {};
|
|
45
|
+
const lines = [];
|
|
46
|
+
lines.push(`{# auto-generated canon-build prompt template — do not hand-edit #}`);
|
|
47
|
+
lines.push(`{{ trigger }} style{% if character_trigger %}, {{ character_trigger }}{% endif %}, a ${fm.kind || fm.role_class || 'figure'}, ${v.silhouette_cue || ''}.`);
|
|
48
|
+
|
|
49
|
+
const identity = [];
|
|
50
|
+
if (v.signature_prop) identity.push(`signature prop: ${v.signature_prop}`);
|
|
51
|
+
if (Array.isArray(v.distinguishing_marks) && v.distinguishing_marks.length) {
|
|
52
|
+
identity.push(`marks: ${v.distinguishing_marks.join(', ')}`);
|
|
53
|
+
}
|
|
54
|
+
if (identity.length) lines.push(`{# identity #} ${identity.join('; ')}`);
|
|
55
|
+
|
|
56
|
+
const body = [];
|
|
57
|
+
if (v.build) body.push(`${v.build} build`);
|
|
58
|
+
if (v.hair) body.push(v.hair);
|
|
59
|
+
if (v.eyes) body.push(`${v.eyes} eyes`);
|
|
60
|
+
if (v.age_band) body.push(`${v.age_band} age`);
|
|
61
|
+
else if (v.age_description) body.push(v.age_description);
|
|
62
|
+
if (body.length) lines.push(`{# body #} ${body.join(', ')}`);
|
|
63
|
+
|
|
64
|
+
if (v.attire) lines.push(`{# attire #} ${v.attire}`);
|
|
65
|
+
if (Array.isArray(v.palette) && v.palette.length) {
|
|
66
|
+
lines.push(`{# palette #} ${v.palette.join(' ')}`);
|
|
67
|
+
}
|
|
68
|
+
if (Array.isArray(fm.signature_features) && fm.signature_features.length) {
|
|
69
|
+
lines.push(`{# signature #} ${fm.signature_features.join(', ')}`);
|
|
70
|
+
}
|
|
71
|
+
if (v.posture_default) {
|
|
72
|
+
lines.push(`{# pose #} ${v.posture_default}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
lines.push('---');
|
|
76
|
+
lines.push(`{{ negative_base }}`);
|
|
77
|
+
if (fm.kind === 'mortal') {
|
|
78
|
+
lines.push(', divine radiance, aureole, supernatural glow');
|
|
79
|
+
}
|
|
80
|
+
if (fm.canon_refs && !fm.sources) {
|
|
81
|
+
// Canon-internal attestation (Star Freight path): callers still expect
|
|
82
|
+
// `{{ negative_base }}` to land on its own line; no extra addition needed.
|
|
83
|
+
}
|
|
84
|
+
return lines.join('\n') + '\n';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function buildContext(entry, ctx) {
|
|
88
|
+
const fm = entry.frontmatter;
|
|
89
|
+
const n = fm.narrative || {};
|
|
90
|
+
const m = fm.mechanical;
|
|
91
|
+
const out = [];
|
|
92
|
+
|
|
93
|
+
out.push(contextFrontmatter({
|
|
94
|
+
entityId: fm.id,
|
|
95
|
+
schemaKind: SCHEMA_KIND,
|
|
96
|
+
generatedFrom: ctx.generatedFrom,
|
|
97
|
+
entryHash: ctx.entryHash,
|
|
98
|
+
}));
|
|
99
|
+
out.push(`# ${titleFromId(fm.id)}\n`);
|
|
100
|
+
|
|
101
|
+
out.push(renderKeyValueSection('Identity', [
|
|
102
|
+
['kind', fm.kind],
|
|
103
|
+
['role_class', fm.role_class],
|
|
104
|
+
['species', fm.species],
|
|
105
|
+
['faction_primary', fm.faction_primary],
|
|
106
|
+
['faction_cover', fm.faction_cover],
|
|
107
|
+
['tech_literacy', fm.tech_literacy],
|
|
108
|
+
['combat_training', fm.combat_training],
|
|
109
|
+
['mortal_status', fm.mortal_status],
|
|
110
|
+
['age_band', fm.visual?.age_band],
|
|
111
|
+
['age_description', fm.visual?.age_description],
|
|
112
|
+
]));
|
|
113
|
+
|
|
114
|
+
out.push(renderKeyValueSection('Role & Voice', [
|
|
115
|
+
['role', n.role],
|
|
116
|
+
['voice', n.voice],
|
|
117
|
+
['motivation', n.motivation],
|
|
118
|
+
['speech_register', n.speech_register],
|
|
119
|
+
['vocabulary_forbidden', n.vocabulary_forbidden],
|
|
120
|
+
]));
|
|
121
|
+
|
|
122
|
+
if (fm.turncoat_arc && fm.turncoat_arc.status && fm.turncoat_arc.status !== 'none') {
|
|
123
|
+
out.push(renderKeyValueSection('Turncoat arc', [
|
|
124
|
+
['status', fm.turncoat_arc.status],
|
|
125
|
+
['reveal_trigger', fm.turncoat_arc.reveal_trigger],
|
|
126
|
+
['defection_threshold', fm.turncoat_arc.defection_threshold],
|
|
127
|
+
['handler_faction', fm.turncoat_arc.handler_faction],
|
|
128
|
+
['canon_true_voice', fm.turncoat_arc.canon_true_voice],
|
|
129
|
+
['seeds', fm.turncoat_arc.seeds],
|
|
130
|
+
]));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (Array.isArray(n.relationships) && n.relationships.length) {
|
|
134
|
+
const relBullets = n.relationships.map((r) => {
|
|
135
|
+
const note = r.note ? ` — ${r.note}` : '';
|
|
136
|
+
return `${r.edge_type} ${r.target_id}${note}`;
|
|
137
|
+
});
|
|
138
|
+
out.push(renderSection('Relationships', relBullets));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
out.push(renderSection('Canonical Events', n.arc_beats || null));
|
|
142
|
+
|
|
143
|
+
if (m && typeof m === 'object' && Object.values(m).some((v) => v != null)) {
|
|
144
|
+
out.push(renderKeyValueSection('Mechanical', [
|
|
145
|
+
['combat_role', m.combat_role],
|
|
146
|
+
['signature_abilities', m.signature_abilities],
|
|
147
|
+
['traits', m.traits],
|
|
148
|
+
]));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
out.push(renderSection('Forbidden', fm.forbidden_inputs || null));
|
|
152
|
+
out.push(renderSection('Sources', fm.sources || null));
|
|
153
|
+
out.push(renderSection('Canon refs', fm.canon_refs || null));
|
|
154
|
+
|
|
155
|
+
if (entry.body && entry.body.trim()) {
|
|
156
|
+
out.push(`## Notes\n${entry.body.trim()}\n`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return out.join('');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function titleFromId(id) {
|
|
163
|
+
if (!id) return 'Untitled';
|
|
164
|
+
return id.split('-').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export { buildCaption as _buildCaption, buildPromptTemplate as _buildPromptTemplate, buildContext as _buildContext };
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deity emitter (D5: deity.schema.json).
|
|
3
|
+
*
|
|
4
|
+
* Caption: trigger + " style, " + generation + " deity, " + silhouette_cue + ", holding " + signature_attribute
|
|
5
|
+
* Prompt positive: silhouette_cue + signature_attribute + epithets + divine_radiance rendered as adjective + body + attire + palette + signature_features + behavior hint
|
|
6
|
+
* Prompt negative: forbidden_inputs + generation-specific guards + radiance=none-on-earth suppression
|
|
7
|
+
* Context sections: Identity, Epithets, Role & Voice, Relationships, Cult, Mechanical (optional), Forbidden, Sources
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { contextFrontmatter, renderKeyValueSection, renderSection } from './base.js';
|
|
11
|
+
|
|
12
|
+
const SCHEMA_KIND = 'deity';
|
|
13
|
+
|
|
14
|
+
const RADIANCE_ADJECTIVES = {
|
|
15
|
+
'subtle': 'with a subtle aura',
|
|
16
|
+
'aureole': 'with a subtle golden halo',
|
|
17
|
+
'golden-aura': 'bathed in golden aura',
|
|
18
|
+
'stormlight': 'crackling with stormlight',
|
|
19
|
+
'underworld-pallor': 'lit by cold underworld pallor',
|
|
20
|
+
'blinding': 'radiant with blinding light',
|
|
21
|
+
'none-on-earth': '',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function emitDeity(entry, ctx) {
|
|
25
|
+
const fm = entry.frontmatter;
|
|
26
|
+
return {
|
|
27
|
+
schemaKind: SCHEMA_KIND,
|
|
28
|
+
caption: buildCaption(fm, ctx),
|
|
29
|
+
prompt: buildPromptTemplate(fm),
|
|
30
|
+
context: buildContext(entry, ctx),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function buildCaption(fm, ctx) {
|
|
35
|
+
const v = fm.visual || {};
|
|
36
|
+
const segments = [];
|
|
37
|
+
if (ctx.trigger) segments.push(`${ctx.trigger} style`);
|
|
38
|
+
|
|
39
|
+
const generation = fm.generation || 'greek';
|
|
40
|
+
const radiance = v.divine_radiance && v.divine_radiance !== 'none-on-earth'
|
|
41
|
+
? `${v.divine_radiance} `
|
|
42
|
+
: '';
|
|
43
|
+
segments.push(`${generation} ${radiance}deity`);
|
|
44
|
+
|
|
45
|
+
if (v.silhouette_cue) segments.push(v.silhouette_cue);
|
|
46
|
+
if (v.signature_attribute) segments.push(`holding ${v.signature_attribute}`);
|
|
47
|
+
|
|
48
|
+
return segments.join(', ');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function buildPromptTemplate(fm) {
|
|
52
|
+
const v = fm.visual || {};
|
|
53
|
+
const n = fm.narrative || {};
|
|
54
|
+
const lines = [];
|
|
55
|
+
lines.push(`{# auto-generated canon-build prompt template — do not hand-edit #}`);
|
|
56
|
+
lines.push(`{{ trigger }} style{% if character_trigger %}, {{ character_trigger }}{% endif %}, a ${fm.generation || 'greek'} deity, ${v.silhouette_cue || ''}.`);
|
|
57
|
+
|
|
58
|
+
if (v.signature_attribute) lines.push(`{# attribute #} holding ${v.signature_attribute}`);
|
|
59
|
+
if (Array.isArray(fm.epithets) && fm.epithets.length) {
|
|
60
|
+
lines.push(`{# epithet #} ${fm.epithets.slice(0, 2).join(', ')}`);
|
|
61
|
+
}
|
|
62
|
+
const radiance = RADIANCE_ADJECTIVES[v.divine_radiance] || '';
|
|
63
|
+
if (radiance) lines.push(`{# radiance #} ${radiance}`);
|
|
64
|
+
|
|
65
|
+
const body = [];
|
|
66
|
+
if (v.build) body.push(`${v.build} build`);
|
|
67
|
+
if (v.hair) body.push(v.hair);
|
|
68
|
+
if (v.age_band) body.push(`${v.age_band} age`);
|
|
69
|
+
if (body.length) lines.push(`{# body #} ${body.join(', ')}`);
|
|
70
|
+
|
|
71
|
+
if (v.attire) lines.push(`{# attire #} ${v.attire}`);
|
|
72
|
+
if (Array.isArray(v.palette) && v.palette.length) {
|
|
73
|
+
lines.push(`{# palette #} ${v.palette.join(' ')}`);
|
|
74
|
+
}
|
|
75
|
+
if (Array.isArray(fm.signature_features) && fm.signature_features.length) {
|
|
76
|
+
lines.push(`{# signature #} ${fm.signature_features.join(', ')}`);
|
|
77
|
+
}
|
|
78
|
+
if (n.core_behavior) {
|
|
79
|
+
lines.push(`{# behavior #} ${firstClause(n.core_behavior)}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
lines.push('---');
|
|
83
|
+
lines.push(`{{ negative_base }}`);
|
|
84
|
+
if (fm.generation === 'olympian') {
|
|
85
|
+
lines.push(', monstrous features, chthonic pallor');
|
|
86
|
+
}
|
|
87
|
+
if (v.divine_radiance === 'none-on-earth') {
|
|
88
|
+
lines.push(', any visible radiance, aureole, glow');
|
|
89
|
+
}
|
|
90
|
+
return lines.join('\n') + '\n';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function buildContext(entry, ctx) {
|
|
94
|
+
const fm = entry.frontmatter;
|
|
95
|
+
const n = fm.narrative || {};
|
|
96
|
+
const m = fm.mechanical;
|
|
97
|
+
const out = [];
|
|
98
|
+
|
|
99
|
+
out.push(contextFrontmatter({
|
|
100
|
+
entityId: fm.id,
|
|
101
|
+
schemaKind: SCHEMA_KIND,
|
|
102
|
+
generatedFrom: ctx.generatedFrom,
|
|
103
|
+
entryHash: ctx.entryHash,
|
|
104
|
+
}));
|
|
105
|
+
out.push(`# ${titleFromId(fm.id)}\n`);
|
|
106
|
+
|
|
107
|
+
out.push(renderKeyValueSection('Identity', [
|
|
108
|
+
['generation', fm.generation],
|
|
109
|
+
['domains', fm.domains],
|
|
110
|
+
]));
|
|
111
|
+
|
|
112
|
+
out.push(renderSection('Epithets', fm.epithets || null));
|
|
113
|
+
|
|
114
|
+
out.push(renderKeyValueSection('Role & Voice', [
|
|
115
|
+
['role', n.role],
|
|
116
|
+
['voice', n.voice],
|
|
117
|
+
['core_behavior', n.core_behavior],
|
|
118
|
+
['alignment_to_hero', n.alignment_to_hero],
|
|
119
|
+
['speech_register', n.speech_register],
|
|
120
|
+
['vocabulary_forbidden', n.vocabulary_forbidden],
|
|
121
|
+
]));
|
|
122
|
+
|
|
123
|
+
out.push(renderKeyValueSection('Relationships', [
|
|
124
|
+
['children', n.children],
|
|
125
|
+
['rivalries', n.rivalries],
|
|
126
|
+
['consorts', n.consorts],
|
|
127
|
+
['parents', fm.parentage?.parents],
|
|
128
|
+
]));
|
|
129
|
+
|
|
130
|
+
out.push(renderKeyValueSection('Cult', [
|
|
131
|
+
['cult_sites', fm.cult_sites],
|
|
132
|
+
['sacred_animals', fm.sacred_animals],
|
|
133
|
+
['sacred_plants', fm.sacred_plants],
|
|
134
|
+
]));
|
|
135
|
+
|
|
136
|
+
if (m && typeof m === 'object' && Object.values(m).some((v) => v != null)) {
|
|
137
|
+
out.push(renderKeyValueSection('Mechanical', [
|
|
138
|
+
['favor_grants', m.favor_grants],
|
|
139
|
+
['wrath_triggers', m.wrath_triggers],
|
|
140
|
+
['encounter_class', m.encounter_class],
|
|
141
|
+
]));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
out.push(renderSection('Forbidden', fm.forbidden_inputs || null));
|
|
145
|
+
out.push(renderSection('Sources', fm.sources || null));
|
|
146
|
+
|
|
147
|
+
if (entry.body && entry.body.trim()) {
|
|
148
|
+
out.push(`## Notes\n${entry.body.trim()}\n`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return out.join('');
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function firstClause(s) {
|
|
155
|
+
if (!s) return '';
|
|
156
|
+
const idx = s.search(/[,;.]/);
|
|
157
|
+
return idx < 0 ? s.trim() : s.slice(0, idx).trim();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function titleFromId(id) {
|
|
161
|
+
if (!id) return 'Untitled';
|
|
162
|
+
return id.split('-').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export { buildCaption as _buildCaption, buildPromptTemplate as _buildPromptTemplate, buildContext as _buildContext };
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Faction emitter (Star Freight D4).
|
|
3
|
+
*
|
|
4
|
+
* Faction captions + prompts serve the World LoRA's faction-aesthetic signal.
|
|
5
|
+
* The emitter lane for factions is constant "faction-aesthetic" (see
|
|
6
|
+
* canon-build/config.json schema_to_lane).
|
|
7
|
+
*
|
|
8
|
+
* Caption: trigger + " style, " + faction_class + " faction aesthetic, " +
|
|
9
|
+
* architectural_style + ", " + aesthetic_markers[0..2]
|
|
10
|
+
* Prompt positive: architectural_style + aesthetic_markers + palette +
|
|
11
|
+
* uniform_or_attire_description + faction_prop_signatures +
|
|
12
|
+
* typography_style + signature_features
|
|
13
|
+
* Prompt negative: forbidden_inputs
|
|
14
|
+
* Context sections: Identity, Visual, Narrative, Stance, Faction edges,
|
|
15
|
+
* Mechanical (optional), Forbidden, Sources, Canon refs
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { contextFrontmatter, renderKeyValueSection, renderSection } from './base.js';
|
|
19
|
+
|
|
20
|
+
const SCHEMA_KIND = 'faction';
|
|
21
|
+
|
|
22
|
+
export function emitFaction(entry, ctx) {
|
|
23
|
+
const fm = entry.frontmatter;
|
|
24
|
+
return {
|
|
25
|
+
schemaKind: SCHEMA_KIND,
|
|
26
|
+
caption: buildCaption(fm, ctx),
|
|
27
|
+
prompt: buildPromptTemplate(fm),
|
|
28
|
+
context: buildContext(entry, ctx),
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function buildCaption(fm, ctx) {
|
|
33
|
+
const v = fm.visual || {};
|
|
34
|
+
const segments = [];
|
|
35
|
+
if (ctx.trigger) segments.push(`${ctx.trigger} style`);
|
|
36
|
+
|
|
37
|
+
const cls = fm.faction_class || 'faction';
|
|
38
|
+
segments.push(`${cls} faction aesthetic`);
|
|
39
|
+
|
|
40
|
+
if (v.architectural_style) segments.push(v.architectural_style);
|
|
41
|
+
|
|
42
|
+
if (Array.isArray(v.aesthetic_markers) && v.aesthetic_markers.length) {
|
|
43
|
+
segments.push(v.aesthetic_markers.slice(0, 2).join(', '));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return segments.join(', ');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildPromptTemplate(fm) {
|
|
50
|
+
const v = fm.visual || {};
|
|
51
|
+
const lines = [];
|
|
52
|
+
lines.push(`{# auto-generated canon-build prompt template — do not hand-edit #}`);
|
|
53
|
+
lines.push(`{{ trigger }} style, ${fm.faction_class || 'faction'} faction aesthetic, ${v.architectural_style || ''}.`);
|
|
54
|
+
|
|
55
|
+
if (Array.isArray(v.aesthetic_markers) && v.aesthetic_markers.length) {
|
|
56
|
+
lines.push(`{# aesthetic #} ${v.aesthetic_markers.join(', ')}`);
|
|
57
|
+
}
|
|
58
|
+
if (v.uniform_or_attire_description) {
|
|
59
|
+
lines.push(`{# uniform #} ${v.uniform_or_attire_description}`);
|
|
60
|
+
}
|
|
61
|
+
if (v.silhouette_convention) {
|
|
62
|
+
lines.push(`{# silhouette #} ${v.silhouette_convention}`);
|
|
63
|
+
}
|
|
64
|
+
if (v.typography_style) {
|
|
65
|
+
lines.push(`{# typography #} ${v.typography_style}`);
|
|
66
|
+
}
|
|
67
|
+
if (Array.isArray(v.faction_prop_signatures) && v.faction_prop_signatures.length) {
|
|
68
|
+
lines.push(`{# props #} ${v.faction_prop_signatures.join(', ')}`);
|
|
69
|
+
}
|
|
70
|
+
if (Array.isArray(v.palette) && v.palette.length) {
|
|
71
|
+
lines.push(`{# palette #} ${v.palette.join(' ')}`);
|
|
72
|
+
}
|
|
73
|
+
if (Array.isArray(fm.signature_features) && fm.signature_features.length) {
|
|
74
|
+
lines.push(`{# signature #} ${fm.signature_features.join(', ')}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
lines.push('---');
|
|
78
|
+
lines.push(`{{ negative_base }}`);
|
|
79
|
+
return lines.join('\n') + '\n';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function buildContext(entry, ctx) {
|
|
83
|
+
const fm = entry.frontmatter;
|
|
84
|
+
const n = fm.narrative || {};
|
|
85
|
+
const s = fm.stance || {};
|
|
86
|
+
const m = fm.mechanical;
|
|
87
|
+
const v = fm.visual || {};
|
|
88
|
+
const out = [];
|
|
89
|
+
|
|
90
|
+
out.push(contextFrontmatter({
|
|
91
|
+
entityId: fm.id,
|
|
92
|
+
schemaKind: SCHEMA_KIND,
|
|
93
|
+
generatedFrom: ctx.generatedFrom,
|
|
94
|
+
entryHash: ctx.entryHash,
|
|
95
|
+
}));
|
|
96
|
+
out.push(`# ${titleFromId(fm.id)}\n`);
|
|
97
|
+
|
|
98
|
+
out.push(renderKeyValueSection('Identity', [
|
|
99
|
+
['faction_class', fm.faction_class],
|
|
100
|
+
]));
|
|
101
|
+
|
|
102
|
+
out.push(renderKeyValueSection('Visual', [
|
|
103
|
+
['palette', v.palette],
|
|
104
|
+
['aesthetic_markers', v.aesthetic_markers],
|
|
105
|
+
['silhouette_convention', v.silhouette_convention],
|
|
106
|
+
['typography_style', v.typography_style],
|
|
107
|
+
['uniform_or_attire_description', v.uniform_or_attire_description],
|
|
108
|
+
['architectural_style', v.architectural_style],
|
|
109
|
+
['faction_prop_signatures', v.faction_prop_signatures],
|
|
110
|
+
]));
|
|
111
|
+
|
|
112
|
+
out.push(renderKeyValueSection('Narrative', [
|
|
113
|
+
['voice', n.voice],
|
|
114
|
+
['values', n.values],
|
|
115
|
+
['taboos', n.taboos],
|
|
116
|
+
['speech_register', n.speech_register],
|
|
117
|
+
['vocabulary_forbidden', n.vocabulary_forbidden],
|
|
118
|
+
['internal_tensions', n.internal_tensions],
|
|
119
|
+
]));
|
|
120
|
+
|
|
121
|
+
out.push(renderKeyValueSection('Stance', [
|
|
122
|
+
['toward_player_at_start', s.toward_player_at_start],
|
|
123
|
+
['toward_player_trajectory', s.toward_player_trajectory],
|
|
124
|
+
]));
|
|
125
|
+
|
|
126
|
+
if (Array.isArray(s.faction_edges) && s.faction_edges.length) {
|
|
127
|
+
const bullets = s.faction_edges.map((r) => {
|
|
128
|
+
const note = r.note ? ` — ${r.note}` : '';
|
|
129
|
+
return `${r.edge_type} ${r.target_id}${note}`;
|
|
130
|
+
});
|
|
131
|
+
out.push(renderSection('Faction edges', bullets));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (m && typeof m === 'object' && Object.values(m).some((vv) => vv != null)) {
|
|
135
|
+
out.push(renderKeyValueSection('Mechanical', [
|
|
136
|
+
['reputation_starting_value', m.reputation_starting_value],
|
|
137
|
+
['skill_bonuses_for_member', m.skill_bonuses_for_member],
|
|
138
|
+
]));
|
|
139
|
+
if (Array.isArray(m.standing_deltas_by_action) && m.standing_deltas_by_action.length) {
|
|
140
|
+
const bullets = m.standing_deltas_by_action.map((d) => `${d.action}: ${d.delta >= 0 ? '+' : ''}${d.delta}`);
|
|
141
|
+
out.push(renderSection('Standing deltas', bullets));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
out.push(renderSection('Forbidden', fm.forbidden_inputs || null));
|
|
146
|
+
out.push(renderSection('Sources', fm.sources || null));
|
|
147
|
+
out.push(renderSection('Canon refs', fm.canon_refs || null));
|
|
148
|
+
|
|
149
|
+
if (entry.body && entry.body.trim()) {
|
|
150
|
+
out.push(`## Notes\n${entry.body.trim()}\n`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return out.join('');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function titleFromId(id) {
|
|
157
|
+
if (!id) return 'Untitled';
|
|
158
|
+
return id.split('-').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export { buildCaption as _buildCaption, buildPromptTemplate as _buildPromptTemplate, buildContext as _buildContext };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Emitter dispatch — schema filename → per-schema emit function.
|
|
3
|
+
*
|
|
4
|
+
* The build orchestrator consults this map to pick the right emitter for an
|
|
5
|
+
* entry. Adding a new canon schema means adding its emitter here; the rest
|
|
6
|
+
* of the build step is schema-agnostic.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { emitMonster } from './monster.js';
|
|
10
|
+
import { emitCharacter } from './character.js';
|
|
11
|
+
import { emitDeity } from './deity.js';
|
|
12
|
+
import { emitLocation } from './location.js';
|
|
13
|
+
import { emitRelic } from './relic.js';
|
|
14
|
+
import { emitFaction } from './faction.js';
|
|
15
|
+
import { emitShip } from './ship.js';
|
|
16
|
+
import { emitSpecies } from './species.js';
|
|
17
|
+
|
|
18
|
+
export const EMITTERS = {
|
|
19
|
+
'monster.schema.json': emitMonster,
|
|
20
|
+
'character.schema.json': emitCharacter,
|
|
21
|
+
'deity.schema.json': emitDeity,
|
|
22
|
+
'location.schema.json': emitLocation,
|
|
23
|
+
'relic.schema.json': emitRelic,
|
|
24
|
+
'faction.schema.json': emitFaction,
|
|
25
|
+
'ship.schema.json': emitShip,
|
|
26
|
+
'species.schema.json': emitSpecies,
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get the emitter for a given schema filename.
|
|
31
|
+
* Returns null when no emitter is registered — orchestrator uses this to
|
|
32
|
+
* surface a structured error rather than silently dropping entries.
|
|
33
|
+
*/
|
|
34
|
+
export function getEmitter(schemaName) {
|
|
35
|
+
return EMITTERS[schemaName] || null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { emitMonster, emitCharacter, emitDeity, emitLocation, emitRelic, emitFaction, emitShip, emitSpecies };
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Location emitter (D5: location.schema.json).
|
|
3
|
+
*
|
|
4
|
+
* Caption: trigger + " style, a " + location_type + " in " + region + ", " + silhouette_cue + ", " + lighting_mood
|
|
5
|
+
* Prompt positive: silhouette_cue + signature_props + lighting_mood + scale_logic + material_language + era_markers + flora_fauna + palette + signature_features
|
|
6
|
+
* Prompt negative: forbidden_inputs (era-anachronism-heavy)
|
|
7
|
+
* Context sections: Identity, Role & Significance, Mythic Events, Associations, Ritual, Forbidden, Sources
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { contextFrontmatter, renderKeyValueSection, renderSection } from './base.js';
|
|
11
|
+
|
|
12
|
+
const SCHEMA_KIND = 'location';
|
|
13
|
+
|
|
14
|
+
export function emitLocation(entry, ctx) {
|
|
15
|
+
const fm = entry.frontmatter;
|
|
16
|
+
return {
|
|
17
|
+
schemaKind: SCHEMA_KIND,
|
|
18
|
+
caption: buildCaption(fm, ctx),
|
|
19
|
+
prompt: buildPromptTemplate(fm),
|
|
20
|
+
context: buildContext(entry, ctx),
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function buildCaption(fm, ctx) {
|
|
25
|
+
const v = fm.visual || {};
|
|
26
|
+
const segments = [];
|
|
27
|
+
if (ctx.trigger) segments.push(`${ctx.trigger} style`);
|
|
28
|
+
|
|
29
|
+
const typeClause = [];
|
|
30
|
+
if (fm.location_type) typeClause.push(`a ${fm.location_type}`);
|
|
31
|
+
const contextTag = fm.region || fm.scale_context?.parent_sector;
|
|
32
|
+
if (contextTag) typeClause.push(`in ${contextTag}`);
|
|
33
|
+
if (typeClause.length) segments.push(typeClause.join(' '));
|
|
34
|
+
|
|
35
|
+
if (v.silhouette_cue) segments.push(v.silhouette_cue);
|
|
36
|
+
if (v.lighting_mood) segments.push(v.lighting_mood);
|
|
37
|
+
|
|
38
|
+
return segments.join(', ');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function buildPromptTemplate(fm) {
|
|
42
|
+
const v = fm.visual || {};
|
|
43
|
+
const lines = [];
|
|
44
|
+
lines.push(`{# auto-generated canon-build prompt template — do not hand-edit #}`);
|
|
45
|
+
const regionTag = fm.region || fm.scale_context?.parent_sector || 'greek lands';
|
|
46
|
+
lines.push(`{{ trigger }} style, a ${fm.location_type || 'place'} in ${regionTag}, ${v.silhouette_cue || ''}.`);
|
|
47
|
+
|
|
48
|
+
if (Array.isArray(v.signature_props) && v.signature_props.length) {
|
|
49
|
+
lines.push(`{# props #} ${v.signature_props.join(', ')}`);
|
|
50
|
+
}
|
|
51
|
+
if (v.lighting_mood) lines.push(`{# light #} ${v.lighting_mood}`);
|
|
52
|
+
if (v.scale_logic) lines.push(`{# scale #} ${v.scale_logic}`);
|
|
53
|
+
if (v.material_language) lines.push(`{# materials #} ${v.material_language}`);
|
|
54
|
+
if (Array.isArray(v.era_markers) && v.era_markers.length) {
|
|
55
|
+
lines.push(`{# era markers #} ${v.era_markers.join(', ')}`);
|
|
56
|
+
}
|
|
57
|
+
if (Array.isArray(v.flora_fauna) && v.flora_fauna.length) {
|
|
58
|
+
lines.push(`{# flora/fauna #} ${v.flora_fauna.join(', ')}`);
|
|
59
|
+
}
|
|
60
|
+
if (Array.isArray(v.palette) && v.palette.length) {
|
|
61
|
+
lines.push(`{# palette #} ${v.palette.join(' ')}`);
|
|
62
|
+
}
|
|
63
|
+
if (Array.isArray(fm.signature_features) && fm.signature_features.length) {
|
|
64
|
+
lines.push(`{# signature #} ${fm.signature_features.join(', ')}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
lines.push('---');
|
|
68
|
+
lines.push(`{{ negative_base }}`);
|
|
69
|
+
return lines.join('\n') + '\n';
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function buildContext(entry, ctx) {
|
|
73
|
+
const fm = entry.frontmatter;
|
|
74
|
+
const n = fm.narrative || {};
|
|
75
|
+
const out = [];
|
|
76
|
+
|
|
77
|
+
out.push(contextFrontmatter({
|
|
78
|
+
entityId: fm.id,
|
|
79
|
+
schemaKind: SCHEMA_KIND,
|
|
80
|
+
generatedFrom: ctx.generatedFrom,
|
|
81
|
+
entryHash: ctx.entryHash,
|
|
82
|
+
}));
|
|
83
|
+
out.push(`# ${titleFromId(fm.id)}\n`);
|
|
84
|
+
|
|
85
|
+
out.push(renderKeyValueSection('Identity', [
|
|
86
|
+
['location_type', fm.location_type],
|
|
87
|
+
['region', fm.region],
|
|
88
|
+
['era_logic', fm.era_logic],
|
|
89
|
+
['parent_ship', fm.scale_context?.parent_ship],
|
|
90
|
+
['parent_station', fm.scale_context?.parent_station],
|
|
91
|
+
['parent_sector', fm.scale_context?.parent_sector],
|
|
92
|
+
['parent_system', fm.scale_context?.parent_system],
|
|
93
|
+
['controlled_by_faction', fm.controlled_by_faction],
|
|
94
|
+
['controlling_species', fm.controlling_species],
|
|
95
|
+
['environment_class', fm.environment_class],
|
|
96
|
+
['access_class', fm.access_class],
|
|
97
|
+
['architectural_style', fm.architectural_style],
|
|
98
|
+
]));
|
|
99
|
+
|
|
100
|
+
out.push(renderKeyValueSection('Role & Significance', [
|
|
101
|
+
['role', n.role],
|
|
102
|
+
['cultural_weight', n.cultural_weight],
|
|
103
|
+
['mortal_access', n.mortal_access],
|
|
104
|
+
]));
|
|
105
|
+
|
|
106
|
+
const events = fm.mythic_events || n.notable_events;
|
|
107
|
+
if (Array.isArray(events) && events.length) {
|
|
108
|
+
const bullets = events.map((ev) => {
|
|
109
|
+
const participants = Array.isArray(ev.participants) && ev.participants.length
|
|
110
|
+
? `, participants: ${ev.participants.join(', ')}`
|
|
111
|
+
: '';
|
|
112
|
+
const cite = ev.source || ev.canon_ref || '';
|
|
113
|
+
return `${ev.event} (${ev.era || ''}${cite ? `, ${cite}` : ''}${participants})`;
|
|
114
|
+
});
|
|
115
|
+
// Preserve the Greek heading when the Greek field is present — tests pin
|
|
116
|
+
// it. SF entries carry `notable_events` (a field Greek doesn't have) and
|
|
117
|
+
// get the new heading.
|
|
118
|
+
const heading = Array.isArray(fm.mythic_events) ? 'Mythic Events' : 'Notable Events';
|
|
119
|
+
out.push(renderSection(heading, bullets));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
out.push(renderKeyValueSection('Associations', [
|
|
123
|
+
['associated_deities', fm.associated_deities],
|
|
124
|
+
['associated_monsters', fm.associated_monsters],
|
|
125
|
+
]));
|
|
126
|
+
|
|
127
|
+
if (Array.isArray(n.relationships) && n.relationships.length) {
|
|
128
|
+
const relBullets = n.relationships.map((r) => {
|
|
129
|
+
const note = r.note ? ` — ${r.note}` : '';
|
|
130
|
+
return `${r.edge_type} ${r.target_id}${note}`;
|
|
131
|
+
});
|
|
132
|
+
out.push(renderSection('Relationships', relBullets));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
out.push(renderSection('Ritual', n.forbidden_acts || null));
|
|
136
|
+
|
|
137
|
+
out.push(renderSection('Forbidden', fm.forbidden_inputs || null));
|
|
138
|
+
out.push(renderSection('Sources', fm.sources || null));
|
|
139
|
+
out.push(renderSection('Canon refs', fm.canon_refs || null));
|
|
140
|
+
|
|
141
|
+
if (entry.body && entry.body.trim()) {
|
|
142
|
+
out.push(`## Notes\n${entry.body.trim()}\n`);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return out.join('');
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function titleFromId(id) {
|
|
149
|
+
if (!id) return 'Untitled';
|
|
150
|
+
return id.split('-').map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export { buildCaption as _buildCaption, buildPromptTemplate as _buildPromptTemplate, buildContext as _buildContext };
|