← All source files

V11 / ca6f28eff240

PersonalitySession.cs

Lilith/Core/Personality/PersonalitySession.cs

Unchanged from the preceding version
CS
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;
        }
    }
}