Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions src/GenWave.Tts/LlmPromptBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ static class LlmPromptBuilder
const int MinSampledQuirks = 2;
const int MaxSampledQuirks = 3;

/// <summary>
/// gh-#188 β€” appended directly under the Quirks line (and only then): tells the model a
/// quirk's inline example ("item four-seven-one-two") demonstrates the bit's SHAPE, not a
/// value to read on air. Public so a spec pins the exact wording next to where it is emitted.
/// </summary>
public const string QuirkExampleGuidance =
"Any example inside a quirk illustrates its style - invent fresh specifics of your own " +
"each break, and never reuse an example's literal values on air.";

/// <summary>
/// Baked house scaffold for the system prompt (SPEC F34.3): personality-neutral radio DJ, 1-2
/// spoken sentences, no stage directions. <paramref name="personaSection"/> (SPEC F35.2, F35.3,
Expand All @@ -34,11 +43,17 @@ static class LlmPromptBuilder
/// </summary>
public static string BuildSystemPrompt(string? personaSection)
{
// gh-#188: the old closing line ("You may embellish with genuine knowledge of the track,
// artist, or era.") was a license a small local model cannot safely hold β€” observed live
// renaming an artist on air (LaBarcaDeSua spoken as "Barcarola") and inventing origins
// ("rural Cuba"). Era/genre color stays welcome; specific unprovided facts do not.
const string Scaffold =
"You are a personality-neutral radio DJ writing live station patter. Write exactly one " +
"or two sentences of spoken copy to be read aloud on air. Plain spoken words only - no " +
"stage directions, no emoji, no markdown formatting, no sound-effect cues. You may " +
"embellish with genuine knowledge of the track, artist, or era.";
"stage directions, no emoji, no markdown formatting, no sound-effect cues. You may add " +
"color about the era or genre, but never state specific facts about the artist or track " +
"that you were not given, and never alter the artist's name or the track's title - when " +
"unsure, stay with what the prompt provides.";

return string.IsNullOrEmpty(personaSection) ? Scaffold : $"{Scaffold}\n\n{personaSection}";
}
Expand All @@ -62,7 +77,15 @@ public static string BuildSystemPrompt(string? personaSection)
{
var sampled = SampleQuirks(card.Quirks);
if (sampled.Count > 0)
{
lines.Add($"Quirks: {string.Join("; ", sampled)}");

// gh-#188: quirk examples exert gravitational pull β€” The Archivist's "invented
// catalog number: 'item four-seven-one-two'" aired that literal number break
// after break. Only emitted when quirks are shown: a quirk-less prompt stays
// byte-identical to its pre-gh-#188 shape.
lines.Add(QuirkExampleGuidance);
}
}

return lines.Count == 0 ? null : string.Join('\n', lines);
Expand Down
85 changes: 85 additions & 0 deletions tests/GenWave.Tts.Tests/Specs/Issue188_PromptGuardrails.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// gh-#188 β€” llm: patter confabulated artist facts and parroted quirk example values.
//
// BDD specification β€” xUnit. Two prompt-side guardrails, both observed failing live on the demo
// box (Booth log, 2026-07-28):
// 1. The system scaffold's open embellishment license ("genuine knowledge") produced confident
// fabrication from a small local model β€” an artist renamed on air (LaBarcaDeSua spoken as
// "Barcarola"), invented origins ("rural Cuba", "a field day in Brooklyn's indie scene").
// The license is now guarded: era/genre color yes, unprovided specifics no, names/titles
// never altered.
// 2. Quirk inline examples exert gravitational pull β€” The Archivist's "invented catalog number:
// 'item four-seven-one-two'" aired that literal number break after break. A guidance line now
// rides directly under the Quirks line, and ONLY when quirks are shown.

using GenWave.Core.Domain;

namespace GenWave.Tts.Tests.Specs;

public static class FeaturePromptGuardrails
{
static Persona BuildPersona() => new(1, "DJ Nova", "", "", "", DateTime.UtcNow, DateTime.UtcNow);

static PersonaCard BuildCard(IReadOnlyList<string> quirks) =>
new(
SchemaVersion: 1,
Name: "DJ Nova",
Tagline: "",
Soul: "A washed-up 90s radio jock chasing one more big break.",
Quirks: quirks,
Voice: new VoiceSpec(Engine: "", VoiceId: "", Pace: 1.0, Language: "en"),
EnergyDisposition: 0,
Lore: [],
Corrections: []);

public static class ScenarioEmbellishmentIsGuarded
{
[Fact]
public static void The_scaffold_forbids_unprovided_facts_and_name_alterations()
{
// Given/When the persona-less system prompt is built
var prompt = LlmPromptBuilder.BuildSystemPrompt(personaSection: null);

// Then the guarded license is present
Assert.Contains("never state specific facts about the artist or track", prompt);
Assert.Contains("never alter the artist's name or the track's title", prompt);
}

[Fact]
public static void The_open_ended_genuine_knowledge_license_is_gone()
{
var prompt = LlmPromptBuilder.BuildSystemPrompt(personaSection: null);

// The pre-gh-#188 sentence invited fabrication β€” it must not resurface
Assert.DoesNotContain("genuine knowledge", prompt);
}
}

public static class ScenarioQuirkExamplesAreStyleOnly
{
[Fact]
public static void A_prompt_that_shows_quirks_carries_the_example_guidance_beneath_them()
{
// Given a persona whose card carries quirks
var section = LlmPromptBuilder.BuildPersonaSection(
BuildPersona(), BuildCard(["Assigns every song an invented catalog number: 'item four-seven-one-two'"]));

// Then the guidance rides directly under the Quirks line
Assert.NotNull(section);
var lines = section.Split('\n');
var quirksIndex = Array.FindIndex(lines, line => line.StartsWith("Quirks:", StringComparison.Ordinal));
Assert.True(quirksIndex >= 0, "expected a Quirks line");
Assert.Equal(LlmPromptBuilder.QuirkExampleGuidance, lines[quirksIndex + 1]);
}

[Fact]
public static void A_quirkless_prompt_stays_byte_identical_to_its_pre_guardrail_shape()
{
// Given a persona with no quirks at all
var section = LlmPromptBuilder.BuildPersonaSection(BuildPersona(), BuildCard([]));

// Then no guidance line appears β€” nothing to guard, nothing added
Assert.NotNull(section);
Assert.DoesNotContain(LlmPromptBuilder.QuirkExampleGuidance, section);
}
}
}
Loading