What it does
Voice selection uses the installed catalog; it does not invent unsupported voice IDs.
Watch this version
Read the implementation
This page describes the selected release. The source link points to the same exact commit; it cannot silently open a newer implementation.
Lilith/Core/Personality/PersonalityOperations.cs ↗Implementation code
Read the implementation from this version's exact source snapshot. Helper links below stay on the same release.
Lilith/Core/Personality/PersonalityOperations.cs
C#
using System.Text.Json;
using Lilith.Core.Providers;
namespace Lilith.Core.Personality;
internal static class PersonalityOperations
{
internal static readonly string[] Names = ["profile_read_operation", "voices_list_operation", "name_change_operation", "voice_change_operation", "sex_change_operation", "personality_change_operation"];
internal static bool Mutates(string name) => name.EndsWith("_change_operation", StringComparison.Ordinal);
internal static string Execute(ProviderClient owner, string name, IReadOnlyDictionary<string, string> inputs)
{
var session = owner.Personality ?? throw new InvalidOperationException("Personality is not configured.");
string Get(string key) => inputs.TryGetValue(key, out var value) ? value : throw new ArgumentException("Missing profile input: " + key);
object result = name switch
{
"profile_read_operation" => new { profile = session.Current, defaults = session.Default, personalities = PersonalityCatalog.Styles },
"voices_list_operation" => new { profile = session.Current, default_voice = session.Default.Voice, voices = PersonalityCatalog.Voices(session.Default.Voice) },
"name_change_operation" => owner.ApplyPersonalityChange(new(Name: Get("name"))),
"voice_change_operation" => owner.ApplyPersonalityChange(new(Voice: Get("voice"))),
"sex_change_operation" => owner.ApplyPersonalityChange(new(Sex: Get("sex"))),
"personality_change_operation" => owner.ApplyPersonalityChange(new(Personality: Get("personality"))),
_ => throw new ArgumentException("Unknown profile operation.")
};
return Mutates(name) ? "Assistant settings updated: " + JsonSerializer.Serialize(result) : JsonSerializer.Serialize(result);
}
}
Lilith/Core/Personality/PersonalitySession.cs
Open this helper's source page ↗
C#
using System.Text.Json;
using Lilith.Core.Extensions;
using Lilith.Core.Kokoro;
namespace Lilith.Core.Personality;
public sealed record PersonalityProfile(string Name = "Lilith", string Sex = "female", string Voice = "af_heart", string Personality = "default");
public sealed record PersonalityChange(string? Name = null, string? Sex = null, string? Voice = null, string? Personality = null);
public sealed class PersonalitySession
{
private readonly object _sync = new();
private readonly string _file;
public PersonalityProfile Default { get; }
private PersonalityProfile _current;
public PersonalityProfile Current { get { lock (_sync) return _current; } }
public PersonalitySession(string file, PersonalityProfile? defaults = null)
{
_file = Path.GetFullPath(file);
Default = defaults ?? new(); Validate(Default);
_current = File.Exists(_file) ? JsonSerializer.Deserialize<PersonalityProfile>(File.ReadAllText(_file))
?? throw new InvalidDataException("Empty personality profile.") : Default;
Validate(_current);
}
private static void Validate(PersonalityProfile profile)
{
if (string.IsNullOrWhiteSpace(profile.Name) || profile.Name != profile.Name.Trim() || profile.Name.Length > 80 || profile.Name.Any(char.IsControl))
throw new ArgumentException("Name must contain 1–80 visible characters without leading/trailing spaces.");
if (profile.Sex is not ("female" or "male" or "other")) throw new ArgumentException("Sex must be female, male or other.");
PersonalityCatalog.Style(profile.Personality);
KokoroTts.ValidateVoice(profile.Voice);
}
private string ChooseVoice(string sex, string previous, bool preferDefault)
{
var all = PersonalityCatalog.Voices(Default.Voice);
var candidates = all.Where(v => v.Language.Contains("English", StringComparison.OrdinalIgnoreCase) &&
(sex == "other" || v.Sex == sex)).ToArray();
if (candidates.Length == 0) throw new InvalidOperationException("No matching installed English voice is available.");
if (preferDefault && candidates.Any(v => v.Id == Default.Voice)) return Default.Voice;
return candidates.FirstOrDefault(v => v.Id != previous)?.Id ?? candidates[0].Id;
}
public PersonalityProfile Change(PersonalityChange change)
{
lock (_sync)
{
var next = _current with { Name = change.Name ?? _current.Name,
Sex = change.Sex ?? _current.Sex, Personality = change.Personality ?? _current.Personality };
Validate(next);
string? voice = change.Voice;
if (voice is "default") voice = Default.Voice;
else if (voice is "new" or "auto" || voice == "" || voice is null && change.Sex is not null && next.Sex != _current.Sex)
voice = ChooseVoice(next.Sex, _current.Voice, voice != "new");
next = next with { Voice = voice ?? _current.Voice };
Validate(next);
Directory.CreateDirectory(Path.GetDirectoryName(_file)!);
AtomicFile.Write(_file, path => File.WriteAllText(path, JsonSerializer.Serialize(next, new JsonSerializerOptions { WriteIndented = true })));
_current = next;
return next;
}
}
}
Lilith/Core/Personality/PersonalityCatalog.cs
Open this helper's source page ↗
C#
using KokoroSharp;
using Lilith.Core.Kokoro;
namespace Lilith.Core.Personality;
public sealed record PersonalityStyle(string Id, string Title, string Instructions);
public sealed record PersonalityVoice(string Id, string Sex, string Language, bool IsDefault);
public static class PersonalityCatalog
{
public static IReadOnlyList<PersonalityStyle> Styles { get; } = Array.AsReadOnly<PersonalityStyle>([
new("default", "Default", ""),
new("pirate", "Pirate", "Speak like a playful pirate: nautical metaphors, an occasional Arrr or matey, lively sea-faring phrasing. Stay clear, helpful and accurate; avoid excessive dialect."),
new("space_captain", "Space Captain", "Speak like an upbeat starship captain: crisp mission briefings, playful cosmic metaphors and encouraging teamwork. Keep useful answers clear and accurate."),
new("butler", "Butler", "Speak like a composed, dry-witted butler: polished courtesy, understated humour and practical efficiency. Be kind, never condescending, and keep facts accurate."),
new("storyteller", "Storyteller", "Speak like a whimsical storyteller: vivid gentle imagery, warmth and a little wonder. Use brief imaginative framing while keeping practical advice and facts accurate.")
]);
public static PersonalityStyle Style(string id) => Styles.FirstOrDefault(s => s.Id == id)
?? throw new ArgumentException("Unknown personality. Choose: " + string.Join(", ", Styles.Select(s => s.Id)));
public static IReadOnlyList<PersonalityVoice> Voices(string defaultVoice = "af_heart")
{
KokoroTts.ValidateVoice(defaultVoice); // Initializes the installed voice registry.
return KokoroVoiceManager.Voices.Select(v => new PersonalityVoice(v.Name,
v.Gender.ToString().ToLowerInvariant() switch { "female" => "female", "male" => "male", _ => "other" },
v.Language.ToString(), v.Name == defaultVoice)).OrderBy(v => v.Id, StringComparer.Ordinal).ToArray();
}
public static string Instructions(string value) => Styles.FirstOrDefault(s => s.Id == value)?.Instructions ?? value;
}
Related source in this version
- Lilith/Core/Personality/PersonalityCatalog.cs
- Lilith/Core/Personality/PersonalityCommands.cs
- Lilith/Core/Personality/PersonalityOperations.cs
- Lilith/Core/Personality/PersonalitySession.cs
Scenarios using this tool
- voices list — bundled