Merge branch 'phase/66-scene-prompt'
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
using HSchool.Content;
|
||||
|
||||
namespace HSchool.Content.Tests;
|
||||
|
||||
public class ScenePromptDefTests
|
||||
{
|
||||
private readonly CatalogLoader _loader = new();
|
||||
|
||||
[Fact]
|
||||
public void VanillaCore_BlackboardHasPositive_ChairIsSilent()
|
||||
{
|
||||
var catalog = LoadVanilla();
|
||||
|
||||
var blackboard = catalog.Things["Blackboard"].Prompt;
|
||||
Assert.NotNull(blackboard);
|
||||
Assert.False(blackboard.IsSilent);
|
||||
Assert.Contains("blackboard", blackboard.Positive, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
var chair = catalog.Things["Chair"].Prompt;
|
||||
Assert.True(chair is null || chair.IsSilent);
|
||||
|
||||
var classroom = catalog.Rooms["Classroom"].Prompt;
|
||||
Assert.NotNull(classroom);
|
||||
Assert.False(classroom.IsSilent);
|
||||
|
||||
var snow = catalog.ClimatePresets["TemperateContinental"].Prompt?.Snow;
|
||||
Assert.NotNull(snow);
|
||||
Assert.False(snow.IsSilent);
|
||||
var clear = catalog.ClimatePresets["TemperateContinental"].Prompt?.Clear;
|
||||
Assert.True(clear is null || clear.IsSilent);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PromptLongerThan120_FailsTheCatalog()
|
||||
{
|
||||
var documents = PackDocuments.FromDirectory(
|
||||
CatalogLoader.CorePackId,
|
||||
Path.Combine(AppContext.BaseDirectory, "vanilla"))
|
||||
.Append(PackDocuments.Def(
|
||||
CatalogLoader.CorePackId,
|
||||
"things",
|
||||
"long-board",
|
||||
$$"""
|
||||
{
|
||||
"defName": "LongBoard",
|
||||
"prompt": { "positive": "{{new string('x', 121)}}" }
|
||||
}
|
||||
"""))
|
||||
.ToList();
|
||||
|
||||
var ex = Assert.Throws<ContentLoadException>(() => _loader.Load([CatalogLoader.CorePackId], documents));
|
||||
Assert.Contains("120", ex.Message, StringComparison.Ordinal);
|
||||
Assert.Contains("LongBoard", ex.Message, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NegativePromptWeight_FailsTheCatalog()
|
||||
{
|
||||
var documents = PackDocuments.FromDirectory(
|
||||
CatalogLoader.CorePackId,
|
||||
Path.Combine(AppContext.BaseDirectory, "vanilla"))
|
||||
.Append(PackDocuments.Def(
|
||||
CatalogLoader.CorePackId,
|
||||
"things",
|
||||
"bad-weight",
|
||||
"""{ "defName": "BadWeight", "prompt": { "positive": "ok", "weight": -0.1 } }"""))
|
||||
.ToList();
|
||||
|
||||
var ex = Assert.Throws<ContentLoadException>(() => _loader.Load([CatalogLoader.CorePackId], documents));
|
||||
Assert.Contains("weight", ex.Message, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private DefCatalog LoadVanilla()
|
||||
{
|
||||
var root = Path.Combine(AppContext.BaseDirectory, "vanilla");
|
||||
return _loader.Load([CatalogLoader.CorePackId], PackDocuments.FromDirectory(CatalogLoader.CorePackId, root));
|
||||
}
|
||||
}
|
||||
@@ -109,6 +109,65 @@ public class PortraitPromptBuilderTests
|
||||
Assert.Contains("wearing a torn white shirt", positive, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_AppendsSceneAfterClothing()
|
||||
{
|
||||
var profile = SamplePreset().ToProfile(PortraitKind.Avatar);
|
||||
var scene = new PortraitScene(
|
||||
["a classroom blackboard with chalk notes"],
|
||||
[],
|
||||
[],
|
||||
[],
|
||||
[new SwarmUiLoraEntry { Name = "winter-embed", Weight = 1 }],
|
||||
[]);
|
||||
var (positive, _) = PortraitPromptBuilder.Build(SampleCard(), profile, PortraitKind.Avatar, scene: scene);
|
||||
|
||||
var clothesAt = positive.IndexOf("wearing a white shirt", StringComparison.OrdinalIgnoreCase);
|
||||
var sceneAt = positive.IndexOf("a classroom blackboard with chalk notes", StringComparison.Ordinal);
|
||||
Assert.True(clothesAt >= 0 && sceneAt > clothesAt);
|
||||
Assert.Contains("<embed:winter-embed>", positive, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("snow.safetensors", positive, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_DoesNotPutSceneLoraInText_ApplySceneAddsLoraParams()
|
||||
{
|
||||
var profile = SamplePreset().ToProfile(PortraitKind.Avatar);
|
||||
var scene = new PortraitScene(
|
||||
["a winter prop"],
|
||||
[],
|
||||
[new SwarmUiLoraEntry { Name = "snow.safetensors", Weight = 0.7 }],
|
||||
[],
|
||||
[],
|
||||
[]);
|
||||
var (positive, _) = PortraitPromptBuilder.Build(SampleCard(), profile, PortraitKind.Avatar, scene: scene);
|
||||
var withLoras = PortraitPromptBuilder.ApplyScene(profile, scene);
|
||||
|
||||
Assert.Contains("a winter prop", positive, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("snow.safetensors", positive, StringComparison.Ordinal);
|
||||
Assert.DoesNotContain("<lora:", positive, StringComparison.Ordinal);
|
||||
Assert.Contains(withLoras.PositiveLoras, entry => entry.Name == "snow.safetensors");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Build_PromptAndGenerateShareTheSameAssembly()
|
||||
{
|
||||
var profile = SamplePreset().ToProfile(PortraitKind.Avatar);
|
||||
var scene = new PortraitScene(
|
||||
["a classroom blackboard with chalk notes"],
|
||||
["messy desk"],
|
||||
[new SwarmUiLoraEntry { Name = "snow.safetensors", Weight = 0.7 }],
|
||||
[],
|
||||
[new SwarmUiLoraEntry { Name = "winter-embed", Weight = 1 }],
|
||||
[]);
|
||||
var prompt = PortraitPromptBuilder.Build(SampleCard(), profile, PortraitKind.Avatar, scene: scene);
|
||||
var generate = PortraitPromptBuilder.Build(SampleCard(), profile, PortraitKind.Avatar, scene: scene);
|
||||
|
||||
Assert.Equal(prompt, generate);
|
||||
Assert.Contains("<embed:winter-embed>", prompt.Positive, StringComparison.Ordinal);
|
||||
Assert.Contains("messy desk", prompt.Negative, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static SwarmUiPresetDefinition SamplePreset()
|
||||
{
|
||||
var preset = SwarmUiPresetDefinition.CreateDefault();
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
using HSchool.Content;
|
||||
using HSchool.Server.Game;
|
||||
using HSchool.Simulation;
|
||||
|
||||
namespace HSchool.Server.Tests;
|
||||
|
||||
public class PortraitSceneCollectorTests
|
||||
{
|
||||
private static readonly SimulationOptions Budget = new();
|
||||
private readonly CatalogLoader _loader = new();
|
||||
|
||||
[Fact]
|
||||
public void Classroom_IncludesBlackboardPhrase_OfficeWithoutBoardDoesNot()
|
||||
{
|
||||
var catalog = LoadCatalog();
|
||||
var map = SampleMap();
|
||||
|
||||
var withBoard = PortraitSceneCollector.Collect(
|
||||
catalog, map, "class-1", Precipitation.None, "TestClimate", [], [], Budget);
|
||||
var withoutBoard = PortraitSceneCollector.Collect(
|
||||
catalog, map, "chairs", Precipitation.None, "TestClimate", [], [], Budget);
|
||||
|
||||
Assert.Contains(withBoard.PositiveFragments, fragment => fragment.Contains("blackboard", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.DoesNotContain(withoutBoard.PositiveFragments, fragment => fragment.Contains("blackboard", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.DoesNotContain(withoutBoard.PositiveFragments, fragment => fragment.Contains("chair", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void FillBlackboard_IsIncluded_ChairFillIsSilent()
|
||||
{
|
||||
var catalog = LoadCatalog();
|
||||
var map = SampleMap();
|
||||
|
||||
var office = PortraitSceneCollector.Collect(
|
||||
catalog, map, "office", Precipitation.None, "TestClimate", [], [], Budget);
|
||||
|
||||
Assert.Contains(office.PositiveFragments, fragment => fragment.Contains("blackboard", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.DoesNotContain(office.PositiveFragments, fragment => fragment.Contains("chair", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Snow_OnlyOutdoors()
|
||||
{
|
||||
var catalog = LoadCatalog();
|
||||
var map = SampleMap();
|
||||
|
||||
var yard = PortraitSceneCollector.Collect(
|
||||
catalog, map, "yard", Precipitation.Snow, "TestClimate", [], [], Budget);
|
||||
var indoor = PortraitSceneCollector.Collect(
|
||||
catalog, map, "class-1", Precipitation.Snow, "TestClimate", [], [], Budget);
|
||||
|
||||
Assert.Contains(yard.PositiveFragments, fragment => fragment.Contains("snow", StringComparison.OrdinalIgnoreCase));
|
||||
Assert.DoesNotContain(indoor.PositiveFragments, fragment => fragment.Contains("snow", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MoreThanSixCandidates_KeepsTopWeightsThenDefName()
|
||||
{
|
||||
var catalog = LoadCatalog();
|
||||
var map = SampleMap();
|
||||
var options = new SimulationOptions { PortraitSceneFragmentLimit = 6 };
|
||||
|
||||
var scene = PortraitSceneCollector.Collect(
|
||||
catalog, map, "ranked", Precipitation.None, "TestClimate", [], [], options);
|
||||
|
||||
Assert.Equal(6, scene.PositiveFragments.Count);
|
||||
Assert.Contains("frag-w7", scene.PositiveFragments);
|
||||
Assert.Contains("frag-w2", scene.PositiveFragments);
|
||||
Assert.DoesNotContain("frag-w1", scene.PositiveFragments);
|
||||
Assert.Equal(
|
||||
["frag-w7", "frag-w6", "frag-w5", "frag-w4", "frag-w3", "frag-w2"],
|
||||
scene.PositiveFragments);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EqualWeight_BreaksTiesByDefName()
|
||||
{
|
||||
var catalog = LoadCatalog();
|
||||
var map = SampleMap();
|
||||
var options = new SimulationOptions { PortraitSceneFragmentLimit = 1 };
|
||||
|
||||
var scene = PortraitSceneCollector.Collect(
|
||||
catalog, map, "tie", Precipitation.None, "TestClimate", [], [], options);
|
||||
|
||||
Assert.Equal(["alpha-tie"], scene.PositiveFragments);
|
||||
Assert.DoesNotContain("zeta-tie", scene.PositiveFragments);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SceneLora_GoesToLoras_EmbeddingGoesToTextTags()
|
||||
{
|
||||
var catalog = LoadCatalog();
|
||||
var map = SampleMap();
|
||||
|
||||
var scene = PortraitSceneCollector.Collect(
|
||||
catalog, map, "props", Precipitation.None, "TestClimate", [], [], Budget);
|
||||
|
||||
Assert.Equal(["snow.safetensors"], scene.PositiveLoras.Select(entry => entry.Name));
|
||||
Assert.Equal(["winter-embed"], scene.PositiveEmbeddings.Select(entry => entry.Name));
|
||||
Assert.DoesNotContain(scene.PositiveFragments, fragment => fragment.Contains("snow.safetensors", StringComparison.Ordinal));
|
||||
Assert.DoesNotContain(scene.PositiveFragments, fragment => fragment.Contains("<embed:", StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
private DefCatalog LoadCatalog()
|
||||
{
|
||||
return _loader.Load(
|
||||
[CatalogLoader.CorePackId],
|
||||
[
|
||||
new ContentDocument(
|
||||
CatalogLoader.CorePackId,
|
||||
"defs/things/scene.jsonc",
|
||||
"""
|
||||
[
|
||||
{ "defName": "Blackboard", "prompt": { "positive": "a classroom blackboard with chalk notes", "weight": 1 } },
|
||||
{ "defName": "Chair" },
|
||||
{ "defName": "StudentDesk", "pupilSlots": 1 },
|
||||
{ "defName": "W7", "prompt": { "positive": "frag-w7", "weight": 7 } },
|
||||
{ "defName": "W6", "prompt": { "positive": "frag-w6", "weight": 6 } },
|
||||
{ "defName": "W5", "prompt": { "positive": "frag-w5", "weight": 5 } },
|
||||
{ "defName": "W4", "prompt": { "positive": "frag-w4", "weight": 4 } },
|
||||
{ "defName": "W3", "prompt": { "positive": "frag-w3", "weight": 3 } },
|
||||
{ "defName": "W2", "prompt": { "positive": "frag-w2", "weight": 2 } },
|
||||
{ "defName": "W1", "prompt": { "positive": "frag-w1", "weight": 1 } },
|
||||
{ "defName": "AlphaTie", "prompt": { "positive": "alpha-tie", "weight": 1 } },
|
||||
{ "defName": "ZetaTie", "prompt": { "positive": "zeta-tie", "weight": 1 } },
|
||||
{
|
||||
"defName": "Prop",
|
||||
"prompt": {
|
||||
"positive": "a winter prop",
|
||||
"weight": 1,
|
||||
"positiveLoras": [{ "name": "snow.safetensors", "weight": 0.7 }],
|
||||
"positiveEmbeddings": [{ "name": "winter-embed", "weight": 1 }]
|
||||
}
|
||||
}
|
||||
]
|
||||
"""),
|
||||
new ContentDocument(
|
||||
CatalogLoader.CorePackId,
|
||||
"defs/rooms/scene.jsonc",
|
||||
"""
|
||||
[
|
||||
{
|
||||
"defName": "Classroom",
|
||||
"homeroom": true,
|
||||
"seatThing": "StudentDesk",
|
||||
"defaultSeats": 16,
|
||||
"travelMinutes": 0.5,
|
||||
"prompt": { "positive": "a classroom blackboard with chalk notes", "weight": 1 }
|
||||
},
|
||||
{
|
||||
"defName": "Office",
|
||||
"travelMinutes": 0.5,
|
||||
"slots": [
|
||||
{ "key": "board", "thing": "Blackboard" },
|
||||
{ "key": "seat", "thing": "Chair" },
|
||||
{ "key": "a", "thing": "W7" },
|
||||
{ "key": "b", "thing": "W6" },
|
||||
{ "key": "c", "thing": "W5" },
|
||||
{ "key": "d", "thing": "W4" },
|
||||
{ "key": "e", "thing": "W3" },
|
||||
{ "key": "f", "thing": "W2" },
|
||||
{ "key": "g", "thing": "W1" },
|
||||
{ "key": "alpha", "thing": "AlphaTie" },
|
||||
{ "key": "zeta", "thing": "ZetaTie" },
|
||||
{ "key": "prop", "thing": "Prop" }
|
||||
]
|
||||
}
|
||||
]
|
||||
"""),
|
||||
new ContentDocument(
|
||||
CatalogLoader.CorePackId,
|
||||
"defs/territories/yard.jsonc",
|
||||
"""{ "defName": "SchoolYard", "travelMinutes": 3 }"""),
|
||||
new ContentDocument(
|
||||
CatalogLoader.CorePackId,
|
||||
"defs/climates/test.jsonc",
|
||||
"""
|
||||
{
|
||||
"defName": "TestClimate",
|
||||
"monthlyNorms": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
|
||||
"daySpread": 0,
|
||||
"hourSpread": 0,
|
||||
"precipitationChance": 0,
|
||||
"prompt": { "snow": { "positive": "falling snow, winter overcast schoolyard", "weight": 1 } }
|
||||
}
|
||||
"""),
|
||||
]);
|
||||
}
|
||||
|
||||
private static MapLayout SampleMap() =>
|
||||
new()
|
||||
{
|
||||
Territory = new TerritoryNode { Id = "yard", Def = "SchoolYard" },
|
||||
Rooms =
|
||||
[
|
||||
new RoomNode
|
||||
{
|
||||
Id = "class-1",
|
||||
Def = "Classroom",
|
||||
Building = "main",
|
||||
Floor = "f1",
|
||||
Seats = 16,
|
||||
},
|
||||
new RoomNode
|
||||
{
|
||||
Id = "office",
|
||||
Def = "Office",
|
||||
Building = "main",
|
||||
Floor = "f1",
|
||||
Slots = [new SlotFill { Key = "board", Thing = "Blackboard" }],
|
||||
},
|
||||
new RoomNode
|
||||
{
|
||||
Id = "chairs",
|
||||
Def = "Office",
|
||||
Building = "main",
|
||||
Floor = "f1",
|
||||
Slots = [new SlotFill { Key = "seat", Thing = "Chair" }],
|
||||
},
|
||||
new RoomNode
|
||||
{
|
||||
Id = "ranked",
|
||||
Def = "Office",
|
||||
Building = "main",
|
||||
Floor = "f1",
|
||||
Slots =
|
||||
[
|
||||
new SlotFill { Key = "a", Thing = "W7" },
|
||||
new SlotFill { Key = "b", Thing = "W6" },
|
||||
new SlotFill { Key = "c", Thing = "W5" },
|
||||
new SlotFill { Key = "d", Thing = "W4" },
|
||||
new SlotFill { Key = "e", Thing = "W3" },
|
||||
new SlotFill { Key = "f", Thing = "W2" },
|
||||
new SlotFill { Key = "g", Thing = "W1" },
|
||||
],
|
||||
},
|
||||
new RoomNode
|
||||
{
|
||||
Id = "tie",
|
||||
Def = "Office",
|
||||
Building = "main",
|
||||
Floor = "f1",
|
||||
Slots =
|
||||
[
|
||||
new SlotFill { Key = "zeta", Thing = "ZetaTie" },
|
||||
new SlotFill { Key = "alpha", Thing = "AlphaTie" },
|
||||
],
|
||||
},
|
||||
new RoomNode
|
||||
{
|
||||
Id = "props",
|
||||
Def = "Office",
|
||||
Building = "main",
|
||||
Floor = "f1",
|
||||
Slots = [new SlotFill { Key = "prop", Thing = "Prop" }],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user