V9 / PERSONALITY

voices_list_operation

List real installed voices with identity and language metadata, including the configured default.

Scenario onlyIntroduced in V9V9

What it does

Voice selection uses the installed catalog; it does not invent unsupported voice IDs.

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

Scenarios using this tool