V11 / ca6f28eff240
PersonalitySession.cs
Lilith/Core/Personality/PersonalitySession.cs
Unchanged from the preceding versionCS
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;
}
}
}