- Move standalone fragment storage from unencrypted SQLite to the existing encrypted SQLCipher database (journal_cache.db) - Add IDatabaseSessionService/DatabaseSessionService for shared encrypted connection management after authentication - Update fragments table schema: nullable entry_id, add guid column - Reorganize flat Services/ directory (28 files) into 9 domain modules: Ai, Config, Database, Entries, Fragments, Logging, Sidecar, Speech, Vault - Update all namespace declarations and using statements across all projects - Update REFACTORING_SUMMARY.md with all changes Co-Authored-By: Warp <agent@warp.dev>
82 lines
3.3 KiB
C#
82 lines
3.3 KiB
C#
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<SpeechDevicesResultDto> 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<SpeechDeviceDto>();
|
|
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<SpeechTranscribeResultDto> 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);
|
|
}
|
|
}
|