diff --git a/Journal.AI/ServiceCollectionExtensions.cs b/Journal.AI/ServiceCollectionExtensions.cs index a8a40cb..48ddaa6 100644 --- a/Journal.AI/ServiceCollectionExtensions.cs +++ b/Journal.AI/ServiceCollectionExtensions.cs @@ -1,6 +1,5 @@ using Journal.Core.Services.Ai; using Journal.Core.Services.Config; -using Journal.Core.Services.Sidecar; using Microsoft.Extensions.DependencyInjection; namespace Journal.AI; @@ -34,21 +33,6 @@ public static class ServiceCollectionExtensions } } - if (string.Equals(config.AiProvider, "python-sidecar", StringComparison.OrdinalIgnoreCase)) - { - try - { - return new PythonSidecarAiService(config); - } - catch (Exception ex) - { - return new DisabledAiService( - provider: "python-sidecar", - message: $"Python AI sidecar unavailable: {ex.Message}", - healthy: false); - } - } - return new DisabledAiService(config.AiProvider); }); diff --git a/Journal.App/src-tauri/icons/128x128.png b/Journal.App/src-tauri/icons/128x128.png index 6be5e50..2d911cd 100644 Binary files a/Journal.App/src-tauri/icons/128x128.png and b/Journal.App/src-tauri/icons/128x128.png differ diff --git a/Journal.App/src-tauri/icons/128x128@2x.png b/Journal.App/src-tauri/icons/128x128@2x.png index e81bece..8d4a5e8 100644 Binary files a/Journal.App/src-tauri/icons/128x128@2x.png and b/Journal.App/src-tauri/icons/128x128@2x.png differ diff --git a/Journal.App/src-tauri/icons/32x32.png b/Journal.App/src-tauri/icons/32x32.png index a437dd5..7b41a78 100644 Binary files a/Journal.App/src-tauri/icons/32x32.png and b/Journal.App/src-tauri/icons/32x32.png differ diff --git a/Journal.App/src-tauri/icons/64x64.png b/Journal.App/src-tauri/icons/64x64.png new file mode 100644 index 0000000..9286e8d Binary files /dev/null and b/Journal.App/src-tauri/icons/64x64.png differ diff --git a/Journal.App/src-tauri/icons/Square107x107Logo.png b/Journal.App/src-tauri/icons/Square107x107Logo.png index 0ca4f27..a195b03 100644 Binary files a/Journal.App/src-tauri/icons/Square107x107Logo.png and b/Journal.App/src-tauri/icons/Square107x107Logo.png differ diff --git a/Journal.App/src-tauri/icons/Square142x142Logo.png b/Journal.App/src-tauri/icons/Square142x142Logo.png index b81f820..a52d49b 100644 Binary files a/Journal.App/src-tauri/icons/Square142x142Logo.png and b/Journal.App/src-tauri/icons/Square142x142Logo.png differ diff --git a/Journal.App/src-tauri/icons/Square150x150Logo.png b/Journal.App/src-tauri/icons/Square150x150Logo.png index 624c7bf..97bf71f 100644 Binary files a/Journal.App/src-tauri/icons/Square150x150Logo.png and b/Journal.App/src-tauri/icons/Square150x150Logo.png differ diff --git a/Journal.App/src-tauri/icons/Square284x284Logo.png b/Journal.App/src-tauri/icons/Square284x284Logo.png index c021d2b..9388caf 100644 Binary files a/Journal.App/src-tauri/icons/Square284x284Logo.png and b/Journal.App/src-tauri/icons/Square284x284Logo.png differ diff --git a/Journal.App/src-tauri/icons/Square30x30Logo.png b/Journal.App/src-tauri/icons/Square30x30Logo.png index 6219700..6fe3e8a 100644 Binary files a/Journal.App/src-tauri/icons/Square30x30Logo.png and b/Journal.App/src-tauri/icons/Square30x30Logo.png differ diff --git a/Journal.App/src-tauri/icons/Square310x310Logo.png b/Journal.App/src-tauri/icons/Square310x310Logo.png index f9bc048..a15a3f9 100644 Binary files a/Journal.App/src-tauri/icons/Square310x310Logo.png and b/Journal.App/src-tauri/icons/Square310x310Logo.png differ diff --git a/Journal.App/src-tauri/icons/Square44x44Logo.png b/Journal.App/src-tauri/icons/Square44x44Logo.png index d5fbfb2..3ac2272 100644 Binary files a/Journal.App/src-tauri/icons/Square44x44Logo.png and b/Journal.App/src-tauri/icons/Square44x44Logo.png differ diff --git a/Journal.App/src-tauri/icons/Square71x71Logo.png b/Journal.App/src-tauri/icons/Square71x71Logo.png index 63440d7..90d71f5 100644 Binary files a/Journal.App/src-tauri/icons/Square71x71Logo.png and b/Journal.App/src-tauri/icons/Square71x71Logo.png differ diff --git a/Journal.App/src-tauri/icons/Square89x89Logo.png b/Journal.App/src-tauri/icons/Square89x89Logo.png index f3f705a..731faa5 100644 Binary files a/Journal.App/src-tauri/icons/Square89x89Logo.png and b/Journal.App/src-tauri/icons/Square89x89Logo.png differ diff --git a/Journal.App/src-tauri/icons/StoreLogo.png b/Journal.App/src-tauri/icons/StoreLogo.png index 4556388..860f9c2 100644 Binary files a/Journal.App/src-tauri/icons/StoreLogo.png and b/Journal.App/src-tauri/icons/StoreLogo.png differ diff --git a/Journal.App/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml b/Journal.App/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..2ffbf24 --- /dev/null +++ b/Journal.App/src-tauri/icons/android/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/Journal.App/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png b/Journal.App/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..5f45f4d Binary files /dev/null and b/Journal.App/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png differ diff --git a/Journal.App/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png b/Journal.App/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..825369d Binary files /dev/null and b/Journal.App/src-tauri/icons/android/mipmap-hdpi/ic_launcher_foreground.png differ diff --git a/Journal.App/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png b/Journal.App/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png new file mode 100644 index 0000000..42faa23 Binary files /dev/null and b/Journal.App/src-tauri/icons/android/mipmap-hdpi/ic_launcher_round.png differ diff --git a/Journal.App/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png b/Journal.App/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..d37d7af Binary files /dev/null and b/Journal.App/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png differ diff --git a/Journal.App/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png b/Journal.App/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..ba45af7 Binary files /dev/null and b/Journal.App/src-tauri/icons/android/mipmap-mdpi/ic_launcher_foreground.png differ diff --git a/Journal.App/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png b/Journal.App/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png new file mode 100644 index 0000000..7d67494 Binary files /dev/null and b/Journal.App/src-tauri/icons/android/mipmap-mdpi/ic_launcher_round.png differ diff --git a/Journal.App/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png b/Journal.App/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..b503076 Binary files /dev/null and b/Journal.App/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png differ diff --git a/Journal.App/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png b/Journal.App/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..df1c2f3 Binary files /dev/null and b/Journal.App/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_foreground.png differ diff --git a/Journal.App/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png b/Journal.App/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png new file mode 100644 index 0000000..6413a9d Binary files /dev/null and b/Journal.App/src-tauri/icons/android/mipmap-xhdpi/ic_launcher_round.png differ diff --git a/Journal.App/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png b/Journal.App/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..feb3485 Binary files /dev/null and b/Journal.App/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png differ diff --git a/Journal.App/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png b/Journal.App/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..9a4e6a8 Binary files /dev/null and b/Journal.App/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_foreground.png differ diff --git a/Journal.App/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png b/Journal.App/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..3ecdfea Binary files /dev/null and b/Journal.App/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher_round.png differ diff --git a/Journal.App/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png b/Journal.App/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..6802d92 Binary files /dev/null and b/Journal.App/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/Journal.App/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png b/Journal.App/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png new file mode 100644 index 0000000..d49935f Binary files /dev/null and b/Journal.App/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_foreground.png differ diff --git a/Journal.App/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png b/Journal.App/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png new file mode 100644 index 0000000..4ac2a01 Binary files /dev/null and b/Journal.App/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher_round.png differ diff --git a/Journal.App/src-tauri/icons/android/values/ic_launcher_background.xml b/Journal.App/src-tauri/icons/android/values/ic_launcher_background.xml new file mode 100644 index 0000000..ea9c223 --- /dev/null +++ b/Journal.App/src-tauri/icons/android/values/ic_launcher_background.xml @@ -0,0 +1,4 @@ + + + #fff + \ No newline at end of file diff --git a/Journal.App/src-tauri/icons/icon.icns b/Journal.App/src-tauri/icons/icon.icns index 12a5bce..722efed 100644 Binary files a/Journal.App/src-tauri/icons/icon.icns and b/Journal.App/src-tauri/icons/icon.icns differ diff --git a/Journal.App/src-tauri/icons/icon.ico b/Journal.App/src-tauri/icons/icon.ico index 00f68f0..d7d5e76 100644 Binary files a/Journal.App/src-tauri/icons/icon.ico and b/Journal.App/src-tauri/icons/icon.ico differ diff --git a/Journal.App/src-tauri/icons/icon.png b/Journal.App/src-tauri/icons/icon.png index e1cd261..a1712ec 100644 Binary files a/Journal.App/src-tauri/icons/icon.png and b/Journal.App/src-tauri/icons/icon.png differ diff --git a/Journal.App/src-tauri/icons/ios/AppIcon-20x20@1x.png b/Journal.App/src-tauri/icons/ios/AppIcon-20x20@1x.png new file mode 100644 index 0000000..d46466a Binary files /dev/null and b/Journal.App/src-tauri/icons/ios/AppIcon-20x20@1x.png differ diff --git a/Journal.App/src-tauri/icons/ios/AppIcon-20x20@2x-1.png b/Journal.App/src-tauri/icons/ios/AppIcon-20x20@2x-1.png new file mode 100644 index 0000000..4c47628 Binary files /dev/null and b/Journal.App/src-tauri/icons/ios/AppIcon-20x20@2x-1.png differ diff --git a/Journal.App/src-tauri/icons/ios/AppIcon-20x20@2x.png b/Journal.App/src-tauri/icons/ios/AppIcon-20x20@2x.png new file mode 100644 index 0000000..4c47628 Binary files /dev/null and b/Journal.App/src-tauri/icons/ios/AppIcon-20x20@2x.png differ diff --git a/Journal.App/src-tauri/icons/ios/AppIcon-20x20@3x.png b/Journal.App/src-tauri/icons/ios/AppIcon-20x20@3x.png new file mode 100644 index 0000000..b9e2fcc Binary files /dev/null and b/Journal.App/src-tauri/icons/ios/AppIcon-20x20@3x.png differ diff --git a/Journal.App/src-tauri/icons/ios/AppIcon-29x29@1x.png b/Journal.App/src-tauri/icons/ios/AppIcon-29x29@1x.png new file mode 100644 index 0000000..b459572 Binary files /dev/null and b/Journal.App/src-tauri/icons/ios/AppIcon-29x29@1x.png differ diff --git a/Journal.App/src-tauri/icons/ios/AppIcon-29x29@2x-1.png b/Journal.App/src-tauri/icons/ios/AppIcon-29x29@2x-1.png new file mode 100644 index 0000000..536bc3e Binary files /dev/null and b/Journal.App/src-tauri/icons/ios/AppIcon-29x29@2x-1.png differ diff --git a/Journal.App/src-tauri/icons/ios/AppIcon-29x29@2x.png b/Journal.App/src-tauri/icons/ios/AppIcon-29x29@2x.png new file mode 100644 index 0000000..536bc3e Binary files /dev/null and b/Journal.App/src-tauri/icons/ios/AppIcon-29x29@2x.png differ diff --git a/Journal.App/src-tauri/icons/ios/AppIcon-29x29@3x.png b/Journal.App/src-tauri/icons/ios/AppIcon-29x29@3x.png new file mode 100644 index 0000000..f1ddfd7 Binary files /dev/null and b/Journal.App/src-tauri/icons/ios/AppIcon-29x29@3x.png differ diff --git a/Journal.App/src-tauri/icons/ios/AppIcon-40x40@1x.png b/Journal.App/src-tauri/icons/ios/AppIcon-40x40@1x.png new file mode 100644 index 0000000..4c47628 Binary files /dev/null and b/Journal.App/src-tauri/icons/ios/AppIcon-40x40@1x.png differ diff --git a/Journal.App/src-tauri/icons/ios/AppIcon-40x40@2x-1.png b/Journal.App/src-tauri/icons/ios/AppIcon-40x40@2x-1.png new file mode 100644 index 0000000..4348b83 Binary files /dev/null and b/Journal.App/src-tauri/icons/ios/AppIcon-40x40@2x-1.png differ diff --git a/Journal.App/src-tauri/icons/ios/AppIcon-40x40@2x.png b/Journal.App/src-tauri/icons/ios/AppIcon-40x40@2x.png new file mode 100644 index 0000000..4348b83 Binary files /dev/null and b/Journal.App/src-tauri/icons/ios/AppIcon-40x40@2x.png differ diff --git a/Journal.App/src-tauri/icons/ios/AppIcon-40x40@3x.png b/Journal.App/src-tauri/icons/ios/AppIcon-40x40@3x.png new file mode 100644 index 0000000..70c1a37 Binary files /dev/null and b/Journal.App/src-tauri/icons/ios/AppIcon-40x40@3x.png differ diff --git a/Journal.App/src-tauri/icons/ios/AppIcon-512@2x.png b/Journal.App/src-tauri/icons/ios/AppIcon-512@2x.png new file mode 100644 index 0000000..768a82f Binary files /dev/null and b/Journal.App/src-tauri/icons/ios/AppIcon-512@2x.png differ diff --git a/Journal.App/src-tauri/icons/ios/AppIcon-60x60@2x.png b/Journal.App/src-tauri/icons/ios/AppIcon-60x60@2x.png new file mode 100644 index 0000000..70c1a37 Binary files /dev/null and b/Journal.App/src-tauri/icons/ios/AppIcon-60x60@2x.png differ diff --git a/Journal.App/src-tauri/icons/ios/AppIcon-60x60@3x.png b/Journal.App/src-tauri/icons/ios/AppIcon-60x60@3x.png new file mode 100644 index 0000000..68b754d Binary files /dev/null and b/Journal.App/src-tauri/icons/ios/AppIcon-60x60@3x.png differ diff --git a/Journal.App/src-tauri/icons/ios/AppIcon-76x76@1x.png b/Journal.App/src-tauri/icons/ios/AppIcon-76x76@1x.png new file mode 100644 index 0000000..8da1fb1 Binary files /dev/null and b/Journal.App/src-tauri/icons/ios/AppIcon-76x76@1x.png differ diff --git a/Journal.App/src-tauri/icons/ios/AppIcon-76x76@2x.png b/Journal.App/src-tauri/icons/ios/AppIcon-76x76@2x.png new file mode 100644 index 0000000..af1c309 Binary files /dev/null and b/Journal.App/src-tauri/icons/ios/AppIcon-76x76@2x.png differ diff --git a/Journal.App/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png b/Journal.App/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png new file mode 100644 index 0000000..6127e87 Binary files /dev/null and b/Journal.App/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png differ diff --git a/Journal.Core/Models/JournalConfig.cs b/Journal.Core/Models/JournalConfig.cs index 7abcae6..0108f92 100644 --- a/Journal.Core/Models/JournalConfig.cs +++ b/Journal.Core/Models/JournalConfig.cs @@ -22,7 +22,4 @@ public sealed record JournalConfig( string WhisperModelSize, string NlpBackend, string AiProvider, - string PythonExecutable, - string PythonAiSidecarPath, - int AiSidecarTimeoutMs, string GgufModelPath); diff --git a/Journal.Core/ServiceCollectionExtensions.cs b/Journal.Core/ServiceCollectionExtensions.cs index 852cd18..b157dc0 100644 --- a/Journal.Core/ServiceCollectionExtensions.cs +++ b/Journal.Core/ServiceCollectionExtensions.cs @@ -30,35 +30,10 @@ public static class ServiceCollectionExtensions services.AddSingleton(provider => { var config = provider.GetRequiredService().Current; - if (!string.Equals(config.AiProvider, "python-sidecar", StringComparison.OrdinalIgnoreCase)) - return new DisabledAiService(config.AiProvider); - - try - { - return new PythonSidecarAiService(config); - } - catch (Exception ex) - { - return new DisabledAiService( - provider: "python-sidecar", - message: $"Python AI sidecar unavailable: {ex.Message}", - healthy: false); - } - }); - services.AddSingleton(provider => - { - var config = provider.GetRequiredService().Current; - try - { - return new PythonSidecarSpeechService(config); - } - catch (Exception ex) - { - return new DisabledSpeechBridgeService( - provider: "python-sidecar", - message: $"Python speech sidecar unavailable: {ex.Message}"); - } + return new DisabledAiService(config.AiProvider); }); + services.AddSingleton( + new DisabledSpeechBridgeService("none")); services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); diff --git a/Journal.Core/Services/Ai/PythonSidecarAiService.cs b/Journal.Core/Services/Ai/PythonSidecarAiService.cs deleted file mode 100644 index b1c0474..0000000 --- a/Journal.Core/Services/Ai/PythonSidecarAiService.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System.Text.Json; -using Journal.Core.Dtos; -using Journal.Core.Models; -using Journal.Core.Services.Sidecar; - -namespace Journal.Core.Services.Ai; - -public sealed class PythonSidecarAiService : IAiService -{ - private readonly PythonSidecarClient _client; - - public PythonSidecarAiService(JournalConfig config) - { - if (string.IsNullOrWhiteSpace(config.PythonAiSidecarPath)) - throw new ArgumentException("Python AI sidecar path is required."); - if (!File.Exists(config.PythonAiSidecarPath)) - throw new FileNotFoundException($"Python AI sidecar not found: {config.PythonAiSidecarPath}"); - _client = new PythonSidecarClient(config); - } - - public async Task HealthAsync(CancellationToken cancellationToken = default) - { - var data = await _client.SendAsync("health", payload: new { }, cancellationToken); - if (data is not JsonElement payload || payload.ValueKind != JsonValueKind.Object) - return new AiHealthDto("python-sidecar", Enabled: true, Healthy: true, Message: "ok"); - - var provider = payload.TryGetProperty("provider", out var providerNode) - ? providerNode.GetString() ?? "python-sidecar" - : "python-sidecar"; - var message = payload.TryGetProperty("message", out var messageNode) - ? messageNode.GetString() ?? "ok" - : "ok"; - var healthy = !payload.TryGetProperty("healthy", out var healthyNode) || - healthyNode.ValueKind is JsonValueKind.True || - (healthyNode.ValueKind is not JsonValueKind.False); - return new AiHealthDto(provider, Enabled: true, Healthy: healthy, Message: message); - } - - public async Task SummarizeEntryAsync(string content, string? fileStem = null, CancellationToken cancellationToken = default) - { - if (string.IsNullOrWhiteSpace(content)) - throw new ArgumentException("Entry content is required.", nameof(content)); - - var data = await _client.SendAsync("summarize_entry", new { content, file_stem = fileStem }, cancellationToken); - return data?.GetString() ?? ""; - } - - public async Task SummarizeAllAsync(IReadOnlyList entries, CancellationToken cancellationToken = default) - { - entries ??= []; - var data = await _client.SendAsync("summarize_all", new { entries }, cancellationToken); - return data?.GetString() ?? ""; - } - - public async Task ChatAsync(string prompt, CancellationToken cancellationToken = default) - { - if (string.IsNullOrWhiteSpace(prompt)) - throw new ArgumentException("Prompt is required.", nameof(prompt)); - - var data = await _client.SendAsync("chat", new { prompt }, cancellationToken); - return data?.GetString() ?? ""; - } - - public Task ChatWithHistoryAsync(IReadOnlyList<(string Role, string Text)> history, - string prompt, CancellationToken cancellationToken = default) - { - // Python sidecar does not support multi-turn — fall back to single-turn - return ChatAsync(prompt, cancellationToken); - } - - public async Task> EmbedAsync(string content, CancellationToken cancellationToken = default) - { - if (string.IsNullOrWhiteSpace(content)) - throw new ArgumentException("Content is required.", nameof(content)); - - var data = await _client.SendAsync("embed", new { content }, cancellationToken); - if (data is null || data.Value.ValueKind == JsonValueKind.Null) - return []; - - if (data.Value.ValueKind != JsonValueKind.Array) - throw new InvalidOperationException("Python AI sidecar embed response must be a numeric array."); - - var values = new List(); - foreach (var item in data.Value.EnumerateArray()) - { - if (item.ValueKind != JsonValueKind.Number) - throw new InvalidOperationException("Python AI sidecar embed response contains a non-numeric value."); - values.Add(item.GetDouble()); - } - - return values; - } -} diff --git a/Journal.Core/Services/Config/JournalConfigService.cs b/Journal.Core/Services/Config/JournalConfigService.cs index d20f531..c73b3b6 100644 --- a/Journal.Core/Services/Config/JournalConfigService.cs +++ b/Journal.Core/Services/Config/JournalConfigService.cs @@ -22,17 +22,9 @@ public sealed class JournalConfigService : IJournalConfigService nlpBackend = "auto"; var aiProvider = (Environment.GetEnvironmentVariable("JOURNAL_AI_PROVIDER") ?? "llamasharp").Trim().ToLowerInvariant(); - if (aiProvider is not ("none" or "python-sidecar" or "llamasharp")) + if (aiProvider is not ("none" or "llamasharp")) aiProvider = "none"; - var pythonExecutable = Environment.GetEnvironmentVariable("JOURNAL_PYTHON_EXE"); - if (string.IsNullOrWhiteSpace(pythonExecutable)) - pythonExecutable = "python"; - - var defaultAiSidecarPath = Path.Combine(projectRoot, "journal", "ai", "sidecar.py"); - var pythonAiSidecarPath = ResolvePath("JOURNAL_AI_SIDECAR_PATH", defaultAiSidecarPath); - var aiSidecarTimeoutMs = ParseInt("JOURNAL_AI_TIMEOUT_MS", 45000); - return new JournalConfig( ProjectRoot: projectRoot, AppDirectory: appDirectory, @@ -55,9 +47,6 @@ public sealed class JournalConfigService : IJournalConfigService WhisperModelSize: Environment.GetEnvironmentVariable("WHISPER_MODEL_SIZE") ?? "base", NlpBackend: nlpBackend, AiProvider: aiProvider, - PythonExecutable: pythonExecutable, - PythonAiSidecarPath: pythonAiSidecarPath, - AiSidecarTimeoutMs: aiSidecarTimeoutMs, GgufModelPath: ResolveGgufModelPath(projectRoot)); } diff --git a/Journal.Core/Services/Sidecar/PythonSidecarClient.cs b/Journal.Core/Services/Sidecar/PythonSidecarClient.cs deleted file mode 100644 index d744bf8..0000000 --- a/Journal.Core/Services/Sidecar/PythonSidecarClient.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System.Diagnostics; -using System.Text.Json; -using Journal.Core.Models; - -namespace Journal.Core.Services.Sidecar; - -public sealed class PythonSidecarClient(JournalConfig config) -{ - private static readonly JsonSerializerOptions JsonOptions = new() - { - PropertyNameCaseInsensitive = true - }; - - private readonly JournalConfig _config = config; - - public async Task SendAsync(string action, object payload, CancellationToken cancellationToken) - { - var request = JsonSerializer.Serialize(new { action, payload }, JsonOptions); - - using var process = new Process(); - process.StartInfo = new ProcessStartInfo - { - FileName = _config.PythonExecutable, - UseShellExecute = false, - RedirectStandardInput = true, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true, - WorkingDirectory = _config.ProjectRoot - }; - process.StartInfo.ArgumentList.Add(_config.PythonAiSidecarPath); - - if (!process.Start()) - throw new InvalidOperationException("Failed to start Python sidecar process."); - - await process.StandardInput.WriteLineAsync(request); - process.StandardInput.Close(); - - using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - timeoutCts.CancelAfter(_config.AiSidecarTimeoutMs); - - try - { - await process.WaitForExitAsync(timeoutCts.Token); - } - catch (OperationCanceledException) - { - TryKill(process); - throw new TimeoutException($"Python sidecar timed out after {_config.AiSidecarTimeoutMs} ms."); - } - - var stdout = await process.StandardOutput.ReadToEndAsync(); - var stderr = await process.StandardError.ReadToEndAsync(); - var line = LastJsonLine(stdout); - if (string.IsNullOrWhiteSpace(line)) - throw new InvalidOperationException($"Python sidecar returned no JSON response. stderr: {stderr}".Trim()); - - JsonDocument doc; - try - { - doc = JsonDocument.Parse(line); - } - catch (JsonException ex) - { - throw new InvalidOperationException($"Invalid JSON from Python sidecar: {line}", ex); - } - - using (doc) - { - var root = doc.RootElement; - if (!root.TryGetProperty("ok", out var okNode) || okNode.ValueKind != JsonValueKind.True && okNode.ValueKind != JsonValueKind.False) - throw new InvalidOperationException("Python sidecar response missing boolean 'ok' field."); - - if (!okNode.GetBoolean()) - { - var error = root.TryGetProperty("error", out var errorNode) - ? errorNode.GetString() ?? "Unknown sidecar error." - : "Unknown sidecar error."; - throw new InvalidOperationException(error); - } - - if (!root.TryGetProperty("data", out var dataNode)) - return null; - - return dataNode.Clone(); - } - } - - private static string LastJsonLine(string text) - { - var lines = text.Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries); - for (var i = lines.Length - 1; i >= 0; i--) - { - var line = lines[i].Trim(); - if (line.StartsWith('{') && line.EndsWith('}')) - return line; - } - - return ""; - } - - private static void TryKill(Process process) - { - try - { - if (!process.HasExited) - process.Kill(entireProcessTree: true); - } - catch - { - // Ignore cleanup errors while handling timeout/failure path. - } - } -} diff --git a/Journal.Core/Services/Speech/PythonSidecarSpeechService.cs b/Journal.Core/Services/Speech/PythonSidecarSpeechService.cs deleted file mode 100644 index 7936f0d..0000000 --- a/Journal.Core/Services/Speech/PythonSidecarSpeechService.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System.Text.Json; -using Journal.Core.Dtos; -using Journal.Core.Models; -using Journal.Core.Services.Sidecar; - -namespace Journal.Core.Services.Speech; - -public sealed class PythonSidecarSpeechService : ISpeechBridgeService -{ - private readonly PythonSidecarClient _client; - - public PythonSidecarSpeechService(JournalConfig config) - { - if (string.IsNullOrWhiteSpace(config.PythonAiSidecarPath)) - throw new ArgumentException("Python sidecar path is required."); - if (!File.Exists(config.PythonAiSidecarPath)) - throw new FileNotFoundException($"Python sidecar not found: {config.PythonAiSidecarPath}"); - _client = new PythonSidecarClient(config); - } - - public async Task ListDevicesAsync(CancellationToken cancellationToken = default) - { - var data = await _client.SendAsync("speech.devices.list", new { }, cancellationToken); - if (data is null || data.Value.ValueKind != JsonValueKind.Object) - return new SpeechDevicesResultDto([], "Unexpected speech device response from Python sidecar."); - - var warning = data.Value.TryGetProperty("warning", out var warningNode) - ? warningNode.GetString() - : null; - - var devices = new List(); - if (data.Value.TryGetProperty("devices", out var devicesNode) && devicesNode.ValueKind == JsonValueKind.Array) - { - foreach (var device in devicesNode.EnumerateArray()) - { - if (device.ValueKind != JsonValueKind.Object) - continue; - - var index = device.TryGetProperty("index", out var indexNode) && indexNode.ValueKind == JsonValueKind.Number - ? indexNode.GetInt32() - : -1; - var name = device.TryGetProperty("name", out var nameNode) - ? nameNode.GetString() ?? "" - : ""; - devices.Add(new SpeechDeviceDto(index, name)); - } - } - - return new SpeechDevicesResultDto(devices, warning); - } - - public async Task TranscribeAsync( - SpeechTranscribeRequestDto request, - CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(request); - - var data = await _client.SendAsync("speech.transcribe", new - { - audio_base64 = request.AudioBase64, - engine = request.Engine, - whisper_model = request.WhisperModel, - text = request.Text, - simulate_delay_ms = request.SimulateDelayMs - }, cancellationToken); - - if (data is null || data.Value.ValueKind != JsonValueKind.Object) - throw new InvalidOperationException("Python sidecar speech response must be a JSON object."); - - var text = data.Value.TryGetProperty("text", out var textNode) - ? textNode.GetString() ?? "" - : ""; - var engine = data.Value.TryGetProperty("engine", out var engineNode) - ? engineNode.GetString() ?? (request.Engine ?? "whisper") - : (request.Engine ?? "whisper"); - var warning = data.Value.TryGetProperty("warning", out var warningNode) - ? warningNode.GetString() - : null; - return new SpeechTranscribeResultDto(text, engine, warning); - } -} diff --git a/Journal.SmokeTests/GlobalUsings.cs b/Journal.SmokeTests/GlobalUsings.cs index b94867a..7ad9a11 100644 --- a/Journal.SmokeTests/GlobalUsings.cs +++ b/Journal.SmokeTests/GlobalUsings.cs @@ -11,8 +11,8 @@ global using Journal.Core.Services.Database; global using Journal.Core.Services.Entries; global using Journal.Core.Services.Fragments; global using Journal.Core.Services.Logging; -global using Journal.Core.Services.Speech; global using Journal.Core.Services.Sidecar; +global using Journal.Core.Services.Speech; global using Journal.Core.Services.Lists; global using Journal.Core.Services.Todos; global using Journal.Core.Services.Conversations; diff --git a/Journal.SmokeTests/Program.AiSpeechTests.cs b/Journal.SmokeTests/Program.AiSpeechTests.cs index 50f7fce..bd80ced 100644 --- a/Journal.SmokeTests/Program.AiSpeechTests.cs +++ b/Journal.SmokeTests/Program.AiSpeechTests.cs @@ -130,213 +130,5 @@ internal static partial class Program var warning = data.TryGetProperty("Warning", out var warningNode) ? warningNode.GetString() ?? "" : ""; Assert(warning.Contains("disabled", StringComparison.OrdinalIgnoreCase), "Expected disabled speech warning."); } - - static async Task TestPythonSidecarAiServiceJsonLineAsync() - { - var root = Path.Combine(Path.GetTempPath(), "journal-ai-smoke", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(root); - var scriptPath = Path.Combine(root, "fake_ai_sidecar.py"); - File.WriteAllText(scriptPath, """ -import json, sys -request = json.loads(sys.stdin.readline()) -action = request.get("action", "") -print("DEBUG prelude") -if action == "health": - print(json.dumps({"ok": True, "data": {"provider": "python-sidecar", "healthy": True, "message": "ok"}})) -elif action == "summarize_entry": - payload = request.get("payload") or {} - print(json.dumps({"ok": True, "data": "ENTRY::" + str(payload.get("content", ""))})) -elif action == "summarize_all": - payload = request.get("payload") or {} - entries = payload.get("entries") or [] - print(json.dumps({"ok": True, "data": "ALL::" + str(len(entries))})) -elif action == "chat": - payload = request.get("payload") or {} - print(json.dumps({"ok": True, "data": "CHAT::" + str(payload.get("prompt", ""))})) -elif action == "embed": - payload = request.get("payload") or {} - text = str(payload.get("content", "")) - print(json.dumps({"ok": True, "data": [float(len(text)), 2.5, -1.0]})) -else: - print(json.dumps({"ok": False, "error": "unknown action"})) -"""); - - try - { - var config = BuildAiConfig(scriptPath, timeoutMs: 4000); - IAiService service = new PythonSidecarAiService(config); - - var health = await service.HealthAsync(); - Assert(health.Enabled, "Expected enabled=true for python-sidecar health."); - Assert(health.Healthy, "Expected healthy=true from fake sidecar health."); - - var one = await service.SummarizeEntryAsync("hello"); - Assert(one == "ENTRY::hello", "Unexpected summarize_entry response."); - - var all = await service.SummarizeAllAsync(["a", "b", "c"]); - Assert(all == "ALL::3", "Unexpected summarize_all response."); - - var chat = await service.ChatAsync("hello"); - Assert(chat == "CHAT::hello", "Unexpected chat response."); - - var vector = await service.EmbedAsync("hello"); - Assert(vector.Count == 3, "Unexpected embed vector length."); - Assert(Math.Abs(vector[0] - 5d) < 0.0001d, "Unexpected embed vector first value."); - Assert(Math.Abs(vector[1] - 2.5d) < 0.0001d, "Unexpected embed vector second value."); - Assert(Math.Abs(vector[2] + 1.0d) < 0.0001d, "Unexpected embed vector third value."); - } - finally - { - if (Directory.Exists(root)) - Directory.Delete(root, recursive: true); - } - } - - static async Task TestPythonSidecarAiServiceErrorAsync() - { - var root = Path.Combine(Path.GetTempPath(), "journal-ai-smoke", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(root); - var scriptPath = Path.Combine(root, "fake_ai_sidecar_error.py"); - File.WriteAllText(scriptPath, """ -import json, sys -_ = json.loads(sys.stdin.readline()) -print(json.dumps({"ok": False, "error": "simulated failure"})) -"""); - - try - { - var config = BuildAiConfig(scriptPath, timeoutMs: 4000); - IAiService service = new PythonSidecarAiService(config); - - try - { - _ = await service.SummarizeEntryAsync("hello"); - } - catch (InvalidOperationException ex) when (ex.Message.Contains("simulated failure", StringComparison.OrdinalIgnoreCase)) - { - return; - } - - throw new InvalidOperationException("Expected summarize_entry to surface sidecar error."); - } - finally - { - if (Directory.Exists(root)) - Directory.Delete(root, recursive: true); - } - } - - static async Task TestPythonSidecarSpeechServiceNoDevicesAsync() - { - var root = Path.Combine(Path.GetTempPath(), "journal-speech-smoke", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(root); - var scriptPath = Path.Combine(root, "fake_speech_sidecar_nodes.py"); - File.WriteAllText(scriptPath, """ -import json, sys -request = json.loads(sys.stdin.readline()) -action = request.get("action", "") -if action == "speech.devices.list": - print(json.dumps({"ok": True, "data": {"devices": [], "warning": "no devices"}})) -elif action == "speech.transcribe": - payload = request.get("payload") or {} - print(json.dumps({"ok": True, "data": {"text": payload.get("text", ""), "engine": payload.get("engine", "whisper")}})) -else: - print(json.dumps({"ok": False, "error": "unknown action"})) -"""); - - try - { - var config = BuildAiConfig(scriptPath, timeoutMs: 4000); - ISpeechBridgeService service = new PythonSidecarSpeechService(config); - - var devices = await service.ListDevicesAsync(); - Assert(devices.Devices.Count == 0, "Expected empty devices list."); - Assert((devices.Warning ?? "").Contains("no devices", StringComparison.OrdinalIgnoreCase), "Expected no-devices warning."); - - var transcript = await service.TranscribeAsync(new SpeechTranscribeRequestDto(Text: "fixture text", Engine: "whisper")); - Assert(transcript.Text == "fixture text", "Expected passthrough transcript text."); - Assert(transcript.Engine == "whisper", "Expected passthrough transcript engine."); - } - finally - { - if (Directory.Exists(root)) - Directory.Delete(root, recursive: true); - } - } - - static async Task TestPythonSidecarSpeechServiceErrorAsync() - { - var root = Path.Combine(Path.GetTempPath(), "journal-speech-smoke", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(root); - var scriptPath = Path.Combine(root, "fake_speech_sidecar_error.py"); - File.WriteAllText(scriptPath, """ -import json, sys -request = json.loads(sys.stdin.readline()) -action = request.get("action", "") -if action == "speech.transcribe": - print(json.dumps({"ok": False, "error": "engine unavailable"})) -else: - print(json.dumps({"ok": True, "data": {"devices": []}})) -"""); - - try - { - var config = BuildAiConfig(scriptPath, timeoutMs: 4000); - ISpeechBridgeService service = new PythonSidecarSpeechService(config); - - try - { - _ = await service.TranscribeAsync(new SpeechTranscribeRequestDto(Text: "fixture", Engine: "faster-whisper")); - } - catch (InvalidOperationException ex) when (ex.Message.Contains("engine unavailable", StringComparison.OrdinalIgnoreCase)) - { - return; - } - - throw new InvalidOperationException("Expected speech transcribe to surface sidecar engine error."); - } - finally - { - if (Directory.Exists(root)) - Directory.Delete(root, recursive: true); - } - } - - static async Task TestPythonSidecarSpeechServiceTimeoutAsync() - { - var root = Path.Combine(Path.GetTempPath(), "journal-speech-smoke", Guid.NewGuid().ToString("N")); - Directory.CreateDirectory(root); - var scriptPath = Path.Combine(root, "fake_speech_sidecar_timeout.py"); - File.WriteAllText(scriptPath, """ -import json, sys, time -request = json.loads(sys.stdin.readline()) -payload = request.get("payload") or {} -sleep_ms = int(payload.get("simulate_delay_ms") or 0) -time.sleep(max(0, sleep_ms) / 1000.0) -print(json.dumps({"ok": True, "data": {"text": "", "engine": "whisper"}})) -"""); - - try - { - var config = BuildAiConfig(scriptPath, timeoutMs: 100); - ISpeechBridgeService service = new PythonSidecarSpeechService(config); - - try - { - _ = await service.TranscribeAsync(new SpeechTranscribeRequestDto(Text: "fixture", SimulateDelayMs: 500)); - } - catch (TimeoutException) - { - return; - } - - throw new InvalidOperationException("Expected speech transcribe timeout path."); - } - finally - { - if (Directory.Exists(root)) - Directory.Delete(root, recursive: true); - } - } } diff --git a/Journal.SmokeTests/Program.DatabaseConfigTests.cs b/Journal.SmokeTests/Program.DatabaseConfigTests.cs index 5ddcf9d..9dfeb32 100644 --- a/Journal.SmokeTests/Program.DatabaseConfigTests.cs +++ b/Journal.SmokeTests/Program.DatabaseConfigTests.cs @@ -120,9 +120,6 @@ internal static partial class Program Assert(current.SpeechRecognitionEngine == "whisper", "Config SpeechRecognitionEngine default mismatch."); Assert(current.WhisperModelSize == "base", "Config WhisperModelSize default mismatch."); Assert(current.AiProvider == "none", "Config AiProvider default mismatch."); - Assert(current.PythonExecutable == "python", "Config PythonExecutable default mismatch."); - Assert(current.AiSidecarTimeoutMs == 45000, "Config AiSidecarTimeoutMs default mismatch."); - Assert(current.PythonAiSidecarPath.EndsWith(Path.Combine("journal", "ai", "sidecar.py"), StringComparison.OrdinalIgnoreCase), "Config PythonAiSidecarPath default mismatch."); return Task.CompletedTask; } diff --git a/Journal.SmokeTests/Program.Shared.cs b/Journal.SmokeTests/Program.Shared.cs index 1f07488..ed1a5cb 100644 --- a/Journal.SmokeTests/Program.Shared.cs +++ b/Journal.SmokeTests/Program.Shared.cs @@ -160,22 +160,6 @@ fragment three } } - static JournalConfig BuildAiConfig(string sidecarScriptPath, int timeoutMs) - { - var baseConfig = new JournalConfigService().Current; - var pythonExe = Environment.GetEnvironmentVariable("JOURNAL_PYTHON_EXE"); - if (string.IsNullOrWhiteSpace(pythonExe)) - pythonExe = "python"; - - return baseConfig with - { - AiProvider = "python-sidecar", - PythonExecutable = pythonExe, - PythonAiSidecarPath = sidecarScriptPath, - AiSidecarTimeoutMs = timeoutMs - }; - } - static async Task> LoadTransportFixturesAsync() { var path = Path.Combine(AppContext.BaseDirectory, "Fixtures", "transport_cases.json"); diff --git a/Journal.SmokeTests/Program.cs b/Journal.SmokeTests/Program.cs index 88d2c88..3640da5 100644 --- a/Journal.SmokeTests/Program.cs +++ b/Journal.SmokeTests/Program.cs @@ -63,11 +63,6 @@ internal static partial class Program ("Entry ai.embed returns empty vector when disabled", TestEntryAiEmbedDisabledAsync), ("Entry speech.devices.list returns envelope when disabled", TestEntrySpeechDevicesListDisabledAsync), ("Entry speech.transcribe returns envelope when disabled", TestEntrySpeechTranscribeDisabledAsync), - ("Python sidecar AI service parses last JSON line", TestPythonSidecarAiServiceJsonLineAsync), - ("Python sidecar AI service surfaces sidecar errors", TestPythonSidecarAiServiceErrorAsync), - ("Python sidecar speech service handles empty devices payload", TestPythonSidecarSpeechServiceNoDevicesAsync), - ("Python sidecar speech service surfaces unavailable engine errors", TestPythonSidecarSpeechServiceErrorAsync), - ("Python sidecar speech service times out deterministically", TestPythonSidecarSpeechServiceTimeoutAsync), ("Entry vault.load_all succeeds for empty vault directory", TestEntryVaultLoadAllEmptyAsync), ("Entry vault.load_all wrong password keeps database session locked", TestEntryVaultLoadWrongPasswordKeepsSessionLockedAsync), ("Entry vault.clear_data_directory compatibility command succeeds", TestEntryVaultClearDataDirectoryAsync),