548 lines
24 KiB
C#
548 lines
24 KiB
C#
using System.ComponentModel.DataAnnotations;
|
|
using System.Globalization;
|
|
using System.Net;
|
|
using System.Text.RegularExpressions;
|
|
using System.Text.Json;
|
|
using Journal.Core.Dtos;
|
|
using Journal.Core.Models;
|
|
using Journal.Core.Services;
|
|
|
|
namespace Journal.Core;
|
|
|
|
public class Entry
|
|
{
|
|
private readonly IFragmentService _fragments;
|
|
private readonly IEntrySearchService _entrySearch;
|
|
private readonly IVaultStorageService _vaultStorage;
|
|
private readonly IJournalDatabaseService _database;
|
|
private readonly IJournalConfigService _config;
|
|
private readonly IAiService _ai;
|
|
private readonly ISpeechBridgeService _speech;
|
|
private static readonly JsonSerializerOptions JsonOptions = new()
|
|
{
|
|
PropertyNameCaseInsensitive = true
|
|
};
|
|
|
|
public Entry(
|
|
IFragmentService fragments,
|
|
IEntrySearchService entrySearch,
|
|
IVaultStorageService vaultStorage,
|
|
IJournalDatabaseService database,
|
|
IJournalConfigService config,
|
|
IAiService ai,
|
|
ISpeechBridgeService speech)
|
|
{
|
|
_fragments = fragments;
|
|
_entrySearch = entrySearch;
|
|
_vaultStorage = vaultStorage;
|
|
_database = database;
|
|
_config = config;
|
|
_ai = ai;
|
|
_speech = speech;
|
|
}
|
|
|
|
public async Task RunAsync()
|
|
{
|
|
string? line;
|
|
while ((line = Console.ReadLine()) is not null)
|
|
{
|
|
var response = await HandleCommandAsync(line);
|
|
Console.WriteLine(response);
|
|
}
|
|
}
|
|
|
|
public async Task<string> HandleCommandAsync(string json)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(json))
|
|
return Error("Invalid command");
|
|
|
|
Command? cmd;
|
|
try
|
|
{
|
|
cmd = JsonSerializer.Deserialize<Command>(json, JsonOptions);
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
return Error("Invalid command JSON");
|
|
}
|
|
|
|
if (cmd is null || string.IsNullOrWhiteSpace(cmd.Action))
|
|
return Error("Invalid command");
|
|
|
|
var action = cmd.Action.Trim();
|
|
var correlationId = string.IsNullOrWhiteSpace(cmd.CorrelationId)
|
|
? Guid.NewGuid().ToString("N")
|
|
: cmd.CorrelationId.Trim();
|
|
LogStart(action, correlationId, cmd.Payload);
|
|
object? result;
|
|
|
|
try
|
|
{
|
|
switch (action)
|
|
{
|
|
case "fragments.list":
|
|
result = await _fragments.GetAllAsync();
|
|
break;
|
|
case "fragments.get":
|
|
if (!Guid.TryParse(cmd.Id, out var getId))
|
|
return Error("Invalid or missing id");
|
|
result = await _fragments.GetByIdAsync(getId);
|
|
break;
|
|
case "fragments.create":
|
|
var createDto = DeserializePayload<CreateFragmentDto>(cmd.Payload);
|
|
if (createDto is null)
|
|
return Error("Missing or invalid payload");
|
|
result = await _fragments.CreateAsync(createDto);
|
|
break;
|
|
case "fragments.update":
|
|
if (!Guid.TryParse(cmd.Id, out var updateId))
|
|
return Error("Invalid or missing id");
|
|
var updateDto = DeserializePayload<UpdateFragmentDto>(cmd.Payload);
|
|
if (updateDto is null)
|
|
return Error("Missing or invalid payload");
|
|
result = await _fragments.UpdateAsync(updateId, updateDto);
|
|
break;
|
|
case "fragments.delete":
|
|
if (!Guid.TryParse(cmd.Id, out var deleteId))
|
|
return Error("Invalid or missing id");
|
|
result = await _fragments.RemoveAsync(deleteId);
|
|
break;
|
|
case "fragments.search":
|
|
result = await _fragments.SearchAsync(cmd.Type, cmd.Tag);
|
|
break;
|
|
case "search.entries":
|
|
var searchPayload = DeserializePayload<SearchEntriesPayload>(cmd.Payload);
|
|
if (searchPayload is null || string.IsNullOrWhiteSpace(searchPayload.DataDirectory))
|
|
return Error("Missing or invalid payload");
|
|
var searchRequest = new EntrySearchRequestDto(
|
|
DataDirectory: searchPayload.DataDirectory,
|
|
Query: searchPayload.Query,
|
|
Section: searchPayload.Section,
|
|
StartDate: searchPayload.StartDate,
|
|
EndDate: searchPayload.EndDate,
|
|
Tags: searchPayload.Tags,
|
|
Types: searchPayload.Types,
|
|
Checked: searchPayload.Checked,
|
|
Unchecked: searchPayload.Unchecked);
|
|
result = await _entrySearch.SearchEntriesAsync(searchRequest);
|
|
break;
|
|
case "entries.list":
|
|
var listPayload = DeserializePayload<EntryListPayload>(cmd.Payload);
|
|
var listDataDirectory = !string.IsNullOrWhiteSpace(listPayload?.DataDirectory)
|
|
? listPayload.DataDirectory
|
|
: _config.Current.DataDirectory;
|
|
result = ListEntries(listDataDirectory);
|
|
break;
|
|
case "entries.load":
|
|
var loadEntryPayload = DeserializePayload<EntryLoadPayload>(cmd.Payload);
|
|
if (loadEntryPayload is null || string.IsNullOrWhiteSpace(loadEntryPayload.FilePath))
|
|
return Error("Missing or invalid payload");
|
|
result = LoadEntry(loadEntryPayload.FilePath);
|
|
break;
|
|
case "entries.save":
|
|
var saveEntryPayload = DeserializePayload<EntrySavePayload>(cmd.Payload);
|
|
if (saveEntryPayload is null || string.IsNullOrWhiteSpace(saveEntryPayload.Content))
|
|
return Error("Missing or invalid payload");
|
|
result = SaveEntry(saveEntryPayload, _config.Current.DataDirectory);
|
|
break;
|
|
case "config.get":
|
|
result = _config.Current;
|
|
break;
|
|
case "ai.health":
|
|
result = await _ai.HealthAsync();
|
|
break;
|
|
case "ai.summarize_entry":
|
|
var summarizeEntryPayload = DeserializePayload<AiSummarizeEntryPayload>(cmd.Payload);
|
|
if (summarizeEntryPayload is null || string.IsNullOrWhiteSpace(summarizeEntryPayload.Content))
|
|
return Error("Missing or invalid payload");
|
|
result = await _ai.SummarizeEntryAsync(summarizeEntryPayload.Content, summarizeEntryPayload.FileStem);
|
|
break;
|
|
case "ai.summarize_all":
|
|
var summarizeAllPayload = DeserializePayload<AiSummarizeAllPayload>(cmd.Payload);
|
|
if (summarizeAllPayload is null)
|
|
return Error("Missing or invalid payload");
|
|
result = await _ai.SummarizeAllAsync(summarizeAllPayload.Entries ?? []);
|
|
break;
|
|
case "ai.chat":
|
|
var chatPayload = DeserializePayload<AiChatPayload>(cmd.Payload);
|
|
if (chatPayload is null || string.IsNullOrWhiteSpace(chatPayload.Prompt))
|
|
return Error("Missing or invalid payload");
|
|
result = await _ai.ChatAsync(chatPayload.Prompt);
|
|
break;
|
|
case "ai.embed":
|
|
var embedPayload = DeserializePayload<AiEmbedPayload>(cmd.Payload);
|
|
if (embedPayload is null || string.IsNullOrWhiteSpace(embedPayload.Content))
|
|
return Error("Missing or invalid payload");
|
|
result = await _ai.EmbedAsync(embedPayload.Content);
|
|
break;
|
|
case "speech.devices.list":
|
|
result = await _speech.ListDevicesAsync();
|
|
break;
|
|
case "speech.transcribe":
|
|
var speechPayload = DeserializePayload<SpeechTranscribePayload>(cmd.Payload);
|
|
if (speechPayload is null)
|
|
return Error("Missing or invalid payload");
|
|
var audioBase64 = !string.IsNullOrWhiteSpace(speechPayload.AudioBase64)
|
|
? speechPayload.AudioBase64
|
|
: speechPayload.Audio_Base64;
|
|
var text = speechPayload.Text;
|
|
var whisperModel = !string.IsNullOrWhiteSpace(speechPayload.WhisperModel)
|
|
? speechPayload.WhisperModel
|
|
: speechPayload.Whisper_Model;
|
|
var simulateDelayMs = speechPayload.SimulateDelayMs ?? speechPayload.Simulate_Delay_Ms;
|
|
if (string.IsNullOrWhiteSpace(audioBase64) && string.IsNullOrWhiteSpace(text))
|
|
return Error("Missing or invalid payload");
|
|
result = await _speech.TranscribeAsync(new SpeechTranscribeRequestDto(
|
|
AudioBase64: audioBase64,
|
|
Engine: speechPayload.Engine,
|
|
WhisperModel: whisperModel,
|
|
Text: text,
|
|
SimulateDelayMs: simulateDelayMs));
|
|
break;
|
|
case "vault.initialize":
|
|
var initPayload = DeserializePayload<VaultInitializePayload>(cmd.Payload);
|
|
if (initPayload is null || string.IsNullOrWhiteSpace(initPayload.Password) || string.IsNullOrWhiteSpace(initPayload.VaultDirectory))
|
|
return Error("Missing or invalid payload");
|
|
Directory.CreateDirectory(initPayload.VaultDirectory);
|
|
result = true;
|
|
break;
|
|
case "vault.load_all":
|
|
var loadPayload = DeserializePayload<VaultPayload>(cmd.Payload);
|
|
if (loadPayload is null)
|
|
return Error("Missing or invalid payload");
|
|
result = _vaultStorage.LoadAllVaults(loadPayload.Password, loadPayload.VaultDirectory, loadPayload.DataDirectory);
|
|
break;
|
|
case "vault.save_current_month":
|
|
var saveCurrentPayload = DeserializePayload<VaultPayload>(cmd.Payload);
|
|
if (saveCurrentPayload is null)
|
|
return Error("Missing or invalid payload");
|
|
result = _vaultStorage.SaveCurrentMonthVault(
|
|
saveCurrentPayload.Password,
|
|
saveCurrentPayload.VaultDirectory,
|
|
saveCurrentPayload.DataDirectory,
|
|
ParseNowOrDefault(saveCurrentPayload.NowUtc));
|
|
break;
|
|
case "vault.rebuild_all":
|
|
var rebuildPayload = DeserializePayload<VaultPayload>(cmd.Payload);
|
|
if (rebuildPayload is null)
|
|
return Error("Missing or invalid payload");
|
|
_vaultStorage.RebuildAllVaults(rebuildPayload.Password, rebuildPayload.VaultDirectory, rebuildPayload.DataDirectory);
|
|
result = true;
|
|
break;
|
|
case "vault.clear_data_directory":
|
|
var clearPayload = DeserializePayload<ClearDataPayload>(cmd.Payload);
|
|
if (clearPayload is null || string.IsNullOrWhiteSpace(clearPayload.DataDirectory))
|
|
return Error("Missing or invalid payload");
|
|
_vaultStorage.ClearDataDirectory(clearPayload.DataDirectory);
|
|
result = true;
|
|
break;
|
|
case "db.status":
|
|
var dbStatusPayload = DeserializePayload<DatabasePayload>(cmd.Payload);
|
|
if (dbStatusPayload is null || string.IsNullOrWhiteSpace(dbStatusPayload.Password))
|
|
return Error("Missing or invalid payload");
|
|
result = _database.GetStatus(dbStatusPayload.Password, dbStatusPayload.DataDirectory);
|
|
break;
|
|
case "db.initialize_schema":
|
|
var dbInitPayload = DeserializePayload<DatabasePayload>(cmd.Payload);
|
|
if (dbInitPayload is null)
|
|
return Error("Missing or invalid payload");
|
|
var schemaPath = _database.WriteSchemaBootstrap(dbInitPayload.DataDirectory);
|
|
result = new { schemaPath };
|
|
break;
|
|
case "db.hydrate_workspace":
|
|
var dbHydratePayload = DeserializePayload<DatabasePayload>(cmd.Payload);
|
|
if (dbHydratePayload is null || string.IsNullOrWhiteSpace(dbHydratePayload.Password))
|
|
return Error("Missing or invalid payload");
|
|
result = _database.HydrateWorkspace(dbHydratePayload.Password, dbHydratePayload.DataDirectory);
|
|
break;
|
|
default:
|
|
LogFailure(action, correlationId, "unknown_action");
|
|
return Error($"Unknown action: {action}");
|
|
}
|
|
}
|
|
catch (JsonException)
|
|
{
|
|
LogFailure(action, correlationId, "invalid_payload_json");
|
|
return Error("Missing or invalid payload");
|
|
}
|
|
catch (ValidationException ex)
|
|
{
|
|
LogFailure(action, correlationId, "validation", ex.Message);
|
|
return Error(ex.Message);
|
|
}
|
|
catch (ArgumentException ex)
|
|
{
|
|
LogFailure(action, correlationId, "argument", ex.Message);
|
|
return Error(ex.Message);
|
|
}
|
|
catch (TimeoutException ex)
|
|
{
|
|
LogFailure(action, correlationId, "timeout", ex.Message);
|
|
return Error(ex.Message);
|
|
}
|
|
catch (InvalidOperationException ex)
|
|
{
|
|
LogFailure(action, correlationId, "invalid_operation", ex.Message);
|
|
return Error(ex.Message);
|
|
}
|
|
catch (FileNotFoundException ex)
|
|
{
|
|
LogFailure(action, correlationId, "not_found", ex.Message);
|
|
return Error(ex.Message);
|
|
}
|
|
catch
|
|
{
|
|
LogFailure(action, correlationId, "internal_error");
|
|
return Error("Internal error");
|
|
}
|
|
|
|
LogSuccess(action, correlationId);
|
|
return JsonSerializer.Serialize(new { ok = true, data = result });
|
|
}
|
|
|
|
private static string Error(string message)
|
|
=> JsonSerializer.Serialize(new { ok = false, error = message });
|
|
|
|
private void LogStart(string action, string correlationId, JsonElement? payload)
|
|
{
|
|
var redactedPayload = LogRedactor.RedactPayload(payload);
|
|
EmitLog("information", action, correlationId, "start", redactedPayload);
|
|
}
|
|
|
|
private void LogSuccess(string action, string correlationId)
|
|
{
|
|
EmitLog("information", action, correlationId, "success");
|
|
}
|
|
|
|
private void LogFailure(string action, string correlationId, string errorType, string? message = null)
|
|
{
|
|
var details = string.IsNullOrWhiteSpace(message)
|
|
? ""
|
|
: (message!.Length <= 160 ? message : $"{message[..160]}...(truncated)");
|
|
EmitLog("warning", action, correlationId, "failure", errorType: errorType, details: details);
|
|
}
|
|
|
|
private static void EmitLog(
|
|
string level,
|
|
string action,
|
|
string correlationId,
|
|
string outcome,
|
|
object? payload = null,
|
|
string? errorType = null,
|
|
string? details = null)
|
|
{
|
|
if (!ShouldLog(level))
|
|
return;
|
|
|
|
var envelope = new
|
|
{
|
|
timestamp = DateTime.UtcNow.ToString("O", CultureInfo.InvariantCulture),
|
|
level,
|
|
component = nameof(Entry),
|
|
action,
|
|
correlation_id = correlationId,
|
|
outcome,
|
|
error_type = errorType,
|
|
details,
|
|
payload
|
|
};
|
|
Console.Error.WriteLine(JsonSerializer.Serialize(envelope));
|
|
}
|
|
|
|
private static bool ShouldLog(string level)
|
|
{
|
|
var configured = (Environment.GetEnvironmentVariable("JOURNAL_LOG_LEVEL") ?? "warning")
|
|
.Trim()
|
|
.ToLowerInvariant();
|
|
var configuredRank = LogLevelRank(configured);
|
|
var incomingRank = LogLevelRank(level);
|
|
return incomingRank >= configuredRank;
|
|
}
|
|
|
|
private static int LogLevelRank(string level) => level switch
|
|
{
|
|
"trace" => 0,
|
|
"debug" => 1,
|
|
"information" => 2,
|
|
"info" => 2,
|
|
"warning" => 3,
|
|
"warn" => 3,
|
|
"error" => 4,
|
|
"critical" => 5,
|
|
_ => 3
|
|
};
|
|
|
|
private static T? DeserializePayload<T>(JsonElement? payload)
|
|
{
|
|
if (payload is null)
|
|
return default;
|
|
return payload.Value.Deserialize<T>(JsonOptions);
|
|
}
|
|
|
|
private static DateTime ParseNowOrDefault(string? nowUtc)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(nowUtc))
|
|
return DateTime.UtcNow;
|
|
|
|
if (DateTime.TryParse(
|
|
nowUtc,
|
|
CultureInfo.InvariantCulture,
|
|
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
|
|
out var parsed))
|
|
{
|
|
return parsed;
|
|
}
|
|
|
|
throw new ArgumentException("Invalid nowUtc value. Expected ISO date/time.");
|
|
}
|
|
|
|
private static IReadOnlyList<EntryListItem> ListEntries(string dataDirectory)
|
|
{
|
|
if (!Directory.Exists(dataDirectory))
|
|
return [];
|
|
|
|
return Directory.GetFiles(dataDirectory, "*.md")
|
|
.OrderBy(Path.GetFileName, StringComparer.Ordinal)
|
|
.Select(path => new EntryListItem(
|
|
FileName: Path.GetFileName(path),
|
|
FilePath: Path.GetFullPath(path)))
|
|
.ToArray();
|
|
}
|
|
|
|
private static EntryLoadResult LoadEntry(string filePath)
|
|
{
|
|
var normalizedPath = Path.GetFullPath(filePath);
|
|
if (!File.Exists(normalizedPath))
|
|
throw new FileNotFoundException($"Entry file not found: {normalizedPath}");
|
|
|
|
var rawContent = StripRichHtml(File.ReadAllText(normalizedPath));
|
|
var fileStem = Path.GetFileNameWithoutExtension(normalizedPath);
|
|
var entry = JournalParser.ParseJournalContent(rawContent, fileStem);
|
|
|
|
return new EntryLoadResult(
|
|
Date: entry.Date,
|
|
FileName: Path.GetFileName(normalizedPath),
|
|
FilePath: normalizedPath,
|
|
RawContent: entry.RawContent);
|
|
}
|
|
|
|
private static EntrySaveResult SaveEntry(EntrySavePayload payload, string defaultDataDirectory)
|
|
{
|
|
var targetPath = ResolveTargetPath(payload.FilePath, defaultDataDirectory);
|
|
var mode = string.IsNullOrWhiteSpace(payload.Mode) ? "Daily" : payload.Mode.Trim();
|
|
var sanitizedContent = StripRichHtml(payload.Content ?? "");
|
|
Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!);
|
|
|
|
if (string.Equals(mode, "Overwrite", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
File.WriteAllText(targetPath, sanitizedContent);
|
|
return new EntrySaveResult(targetPath);
|
|
}
|
|
|
|
if (string.Equals(mode, "Fragment", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
File.AppendAllText(targetPath, "\n\n" + sanitizedContent.Trim());
|
|
return new EntrySaveResult(targetPath);
|
|
}
|
|
|
|
string finalContent;
|
|
if (File.Exists(targetPath))
|
|
{
|
|
var existingContent = File.ReadAllText(targetPath);
|
|
var fileStem = Path.GetFileNameWithoutExtension(targetPath);
|
|
var existingEntry = JournalParser.ParseJournalContent(existingContent, fileStem);
|
|
var newEntryData = JournalParser.ParseJournalContent(sanitizedContent, fileStem);
|
|
existingEntry.MergeWith(newEntryData);
|
|
finalContent = existingEntry.ToMarkdown();
|
|
}
|
|
else
|
|
{
|
|
finalContent = sanitizedContent;
|
|
}
|
|
|
|
File.WriteAllText(targetPath, finalContent);
|
|
return new EntrySaveResult(targetPath);
|
|
}
|
|
|
|
private static string ResolveTargetPath(string? filePath, string defaultDataDirectory)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(filePath))
|
|
return Path.GetFullPath(filePath);
|
|
|
|
return Path.GetFullPath(Path.Combine(defaultDataDirectory, $"{DateTime.Now:yyyy-MM-dd}.md"));
|
|
}
|
|
|
|
private static bool LooksLikeRichHtml(string content)
|
|
{
|
|
var lowered = content.ToLowerInvariant();
|
|
string[] markers =
|
|
[
|
|
"<p", "</p>", "<div", "<span", "<table", "<tr", "<td", "<li", "<ul", "<ol",
|
|
"style=", "font-family:", "-webkit-text-stroke"
|
|
];
|
|
if (markers.Any(marker => lowered.Contains(marker, StringComparison.Ordinal)))
|
|
return true;
|
|
return Regex.Matches(lowered, "</?[a-z][^>]*>").Count >= 8;
|
|
}
|
|
|
|
private static string StripRichHtml(string content)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(content))
|
|
return content;
|
|
if (!LooksLikeRichHtml(content))
|
|
return content;
|
|
|
|
var text = content.Replace("\r\n", "\n").Replace("\r", "\n");
|
|
text = Regex.Replace(text, "<(script|style)\\b[^>]*>.*?</\\1>", "", RegexOptions.IgnoreCase | RegexOptions.Singleline);
|
|
text = Regex.Replace(text, "<br\\s*/?>", "\n", RegexOptions.IgnoreCase);
|
|
text = Regex.Replace(text, "</(p|div|h[1-6]|tr|table|ul|ol|blockquote)>", "\n", RegexOptions.IgnoreCase);
|
|
text = Regex.Replace(text, "<li\\b[^>]*>", "\n- ", RegexOptions.IgnoreCase);
|
|
text = Regex.Replace(text, "</li>", "\n", RegexOptions.IgnoreCase);
|
|
text = Regex.Replace(text, "<(td|th)\\b[^>]*>", " | ", RegexOptions.IgnoreCase);
|
|
text = Regex.Replace(text, "</(td|th)>", " ", RegexOptions.IgnoreCase);
|
|
text = Regex.Replace(text, "<hr\\b[^>]*>", "\n---\n", RegexOptions.IgnoreCase);
|
|
text = Regex.Replace(text, "<[^>]+>", "", RegexOptions.Singleline);
|
|
text = WebUtility.HtmlDecode(text)
|
|
.Replace('\u00a0', ' ')
|
|
.Replace("\u200b", "", StringComparison.Ordinal);
|
|
text = string.Join("\n", text.Split('\n').Select(line => line.TrimEnd()));
|
|
text = Regex.Replace(text, "[ \\t]{2,}", " ");
|
|
text = Regex.Replace(text, "\n{3,}", "\n\n").Trim();
|
|
return string.IsNullOrEmpty(text) ? content : text;
|
|
}
|
|
|
|
private sealed record VaultInitializePayload(string Password, string VaultDirectory);
|
|
private sealed record VaultPayload(string Password, string VaultDirectory, string DataDirectory, string? NowUtc = null);
|
|
private sealed record ClearDataPayload(string DataDirectory);
|
|
private sealed record EntryListPayload(string? DataDirectory = null);
|
|
private sealed record EntryLoadPayload(string FilePath);
|
|
private sealed record EntrySavePayload(string Content, string? FilePath = null, string? Mode = null);
|
|
private sealed record EntryListItem(string FileName, string FilePath);
|
|
private sealed record EntryLoadResult(string Date, string FileName, string FilePath, string RawContent);
|
|
private sealed record EntrySaveResult(string FilePath);
|
|
private sealed record DatabasePayload(string Password, string? DataDirectory = null);
|
|
private sealed record AiSummarizeEntryPayload(string Content, string? FileStem = null);
|
|
private sealed record AiSummarizeAllPayload(List<string>? Entries);
|
|
private sealed record AiChatPayload(string Prompt);
|
|
private sealed record AiEmbedPayload(string Content);
|
|
private sealed record SpeechTranscribePayload(
|
|
string? AudioBase64 = null,
|
|
string? Audio_Base64 = null,
|
|
string? Engine = null,
|
|
string? WhisperModel = null,
|
|
string? Whisper_Model = null,
|
|
string? Text = null,
|
|
int? SimulateDelayMs = null,
|
|
int? Simulate_Delay_Ms = null);
|
|
private sealed record SearchEntriesPayload(
|
|
string DataDirectory,
|
|
string? Query = null,
|
|
string? Section = null,
|
|
string? StartDate = null,
|
|
string? EndDate = null,
|
|
List<string>? Tags = null,
|
|
List<string>? Types = null,
|
|
List<string>? Checked = null,
|
|
List<string>? Unchecked = null);
|
|
}
|