1783 lines
72 KiB
C#
1783 lines
72 KiB
C#
using System;
|
||
using System.Collections.Concurrent;
|
||
using System.Threading;
|
||
using System.Threading.Tasks;
|
||
using Microsoft.AspNetCore.SignalR;
|
||
using Microsoft.Extensions.Logging;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Collections.Generic;
|
||
using System.Text.Json;
|
||
|
||
using Newtonsoft.Json;
|
||
|
||
using ShengShengBuXi.Models;
|
||
using ShengShengBuXi.Services;
|
||
using System.Xml.Linq;
|
||
using static System.Net.Mime.MediaTypeNames;
|
||
|
||
namespace ShengShengBuXi.Hubs
|
||
{
|
||
/// <summary>
|
||
/// 音频处理Hub,处理WebSocket连接和音频数据传输
|
||
/// </summary>
|
||
public class AudioHub : Hub, IDisposable
|
||
{
|
||
/// <summary>
|
||
///
|
||
/// </summary>
|
||
private readonly IAudioProcessingService _audioProcessingService;
|
||
private readonly IConfigurationService _configurationService;
|
||
private readonly ISpeechToTextService _speechToTextService;
|
||
private static readonly ConcurrentDictionary<string, ClientInfo> _clients = new ConcurrentDictionary<string, ClientInfo>();
|
||
private readonly ILogger<AudioHub> _logger;
|
||
private readonly IHubContext<AudioHub> _hubContext;
|
||
private bool _disposed = false;
|
||
private readonly HashSet<string> _processedRecordingPaths = new HashSet<string>();
|
||
/// <summary>
|
||
/// 使用静态集合进行全局去重,防止服务重启或热重载时丢失去重状态
|
||
/// </summary>
|
||
private static readonly ConcurrentDictionary<string, DateTime> _globalProcessedPaths = new ConcurrentDictionary<string, DateTime>();
|
||
/// <summary>
|
||
/// 用于清理过期记录的计时器
|
||
/// </summary>
|
||
private static Timer _cleanupTimer;
|
||
|
||
/// <summary>
|
||
/// 显示文本队列
|
||
/// </summary>
|
||
private static ConcurrentDictionary<Guid, DisplayText> _displayTextQueue { get; set; } = new ConcurrentDictionary<Guid, DisplayText>();
|
||
/// <summary>
|
||
/// 控制端拦截的文本队列
|
||
/// </summary>
|
||
private static ConcurrentDictionary<Guid, DisplayText> _monitorTextQueue { get; set; } = new ConcurrentDictionary<Guid, DisplayText>();
|
||
|
||
/// <summary>
|
||
/// ai美化文本队列
|
||
/// </summary>
|
||
private static ConcurrentDictionary<Guid, DisplayText> _aiTextQueue { get; set; } = new ConcurrentDictionary<Guid, DisplayText>();
|
||
/// <summary>
|
||
/// 显示文本定时器
|
||
/// </summary>
|
||
private static Timer _displayTextTimer { get; set; }
|
||
/// <summary>
|
||
/// 上次有真实用户说话的时间
|
||
/// </summary>
|
||
private static DateTime _lastRealUserSpeakTime = DateTime.MinValue;
|
||
/// <summary>
|
||
/// 是否已初始化显示文本定时器
|
||
/// </summary>
|
||
private static bool _isDisplayTimerInitialized = false;
|
||
/// <summary>
|
||
/// 显示文本定时器锁
|
||
/// </summary>
|
||
private static readonly object _displayTimerLock = new object();
|
||
/// <summary>
|
||
/// 监控文本队列的持久化文件路径
|
||
/// </summary>
|
||
private static readonly string _monitorTextQueueFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config/monitor_text_queue.json");
|
||
/// <summary>
|
||
/// 预设句子文件路径
|
||
/// </summary>
|
||
private static readonly string _sentencesFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config/sentences.txt");
|
||
/// <summary>
|
||
/// 真实用户显示记录的持久化文件路径
|
||
/// </summary>
|
||
private static readonly string _realUserDisplayLogsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config/real_user_displays.log");
|
||
/// <summary>
|
||
/// 预设句子列表
|
||
/// </summary>
|
||
private static List<string> _presetSentences = new List<string>();
|
||
|
||
// 配置信息
|
||
private static string _displayConfig = "{}";
|
||
private static object _displayConfigLock = new object();
|
||
|
||
// 控屏开关设置
|
||
private static bool _manualScreenControlEnabled = false;
|
||
private static readonly string _screenControlSettingPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config/screen_control.json");
|
||
|
||
// 管理员配置保存路径
|
||
private static readonly string ConfigDirectory = Path.Combine(Directory.GetCurrentDirectory(), "config");
|
||
private static readonly string DisplayConfigPath = Path.Combine(ConfigDirectory, "display.json");
|
||
|
||
private static DateTime _lastRealTimeResultSentTime = DateTime.MinValue;
|
||
private static int _realTimeResultCounter = 0;
|
||
private static readonly TimeSpan _realTimeResultThrottleWindow = TimeSpan.FromSeconds(0.3);
|
||
private static readonly int _maxRealTimeResultsPerSecond = 1;
|
||
|
||
// 增加对最终结果的限流变量
|
||
private static DateTime _lastFinalResultSentTime = DateTime.MinValue;
|
||
private static int _finalResultCounter = 0;
|
||
private static TimeSpan _finalResultThrottleWindow = TimeSpan.FromSeconds(0.3);
|
||
private static readonly int _maxFinalResultsPerSecond = 1;
|
||
|
||
// 用于初始化配置
|
||
static AudioHub()
|
||
{
|
||
// 确保配置目录存在
|
||
if (!Directory.Exists(ConfigDirectory))
|
||
{
|
||
Directory.CreateDirectory(ConfigDirectory);
|
||
}
|
||
|
||
// 加载显示配置
|
||
LoadDisplayConfig();
|
||
|
||
// 加载控屏设置
|
||
LoadScreenControlSetting();
|
||
}
|
||
|
||
// 加载显示配置
|
||
private static void LoadDisplayConfig()
|
||
{
|
||
try
|
||
{
|
||
if (File.Exists(DisplayConfigPath))
|
||
{
|
||
string configJson = File.ReadAllText(DisplayConfigPath);
|
||
lock (_displayConfigLock)
|
||
{
|
||
_displayConfig = configJson;
|
||
}
|
||
Console.WriteLine("加载显示配置成功");
|
||
}
|
||
else
|
||
{
|
||
// 创建默认配置
|
||
string defaultConfig = @"{
|
||
""leftContainer"": {
|
||
""turnPageHeight"": 0.8,
|
||
""fontSize"": ""16px"",
|
||
""typewriterSpeed"": 50
|
||
},
|
||
""rightContainer"": {
|
||
""fontSize"": ""24px"",
|
||
""fontWeight"": ""normal"",
|
||
""fontStyle"": ""normal"",
|
||
""typewriterSpeed"": 50
|
||
},
|
||
""waterEffect"": {
|
||
""enabled"": true,
|
||
""minInterval"": 800,
|
||
""maxInterval"": 2000,
|
||
""simultaneousDrops"": 3,
|
||
""fadeOutSpeed"": 0.1,
|
||
""centerBias"": 0.5,
|
||
""largeDrop"": {
|
||
""probability"": 0.2,
|
||
""size"": 9
|
||
}
|
||
}
|
||
}";
|
||
|
||
lock (_displayConfigLock)
|
||
{
|
||
_displayConfig = defaultConfig;
|
||
}
|
||
|
||
// 保存默认配置
|
||
File.WriteAllText(DisplayConfigPath, defaultConfig);
|
||
Console.WriteLine("创建默认显示配置成功");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"加载显示配置出错: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
// 加载控屏设置
|
||
private static void LoadScreenControlSetting()
|
||
{
|
||
try
|
||
{
|
||
if (File.Exists(_screenControlSettingPath))
|
||
{
|
||
string json = File.ReadAllText(_screenControlSettingPath);
|
||
var setting = JsonConvert.DeserializeObject<dynamic>(json);
|
||
_manualScreenControlEnabled = setting?.IsManual ?? false;
|
||
Console.WriteLine($"加载控屏设置成功,当前模式: {(_manualScreenControlEnabled ? "手动" : "自动")}");
|
||
}
|
||
else
|
||
{
|
||
// 默认设置为自动
|
||
_manualScreenControlEnabled = false;
|
||
|
||
// 保存默认设置
|
||
SaveScreenControlSetting();
|
||
Console.WriteLine("创建默认控屏设置成功");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"加载控屏设置出错: {ex.Message}");
|
||
_manualScreenControlEnabled = false;
|
||
}
|
||
}
|
||
|
||
// 保存控屏设置
|
||
private static bool SaveScreenControlSetting()
|
||
{
|
||
try
|
||
{
|
||
string json = JsonConvert.SerializeObject(new { IsManual = _manualScreenControlEnabled });
|
||
File.WriteAllText(_screenControlSettingPath, json);
|
||
return true;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"保存控屏设置出错: {ex.Message}");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化音频Hub
|
||
/// </summary>
|
||
/// <param name="audioProcessingService">音频处理服务</param>
|
||
/// <param name="configurationService">配置服务</param>
|
||
/// <param name="speechToTextService">语音识别服务</param>
|
||
/// <param name="logger">日志记录器</param>
|
||
/// <param name="hubContext">Hub上下文</param>
|
||
public AudioHub(
|
||
IAudioProcessingService audioProcessingService,
|
||
IConfigurationService configurationService,
|
||
ISpeechToTextService speechToTextService,
|
||
ILogger<AudioHub> logger,
|
||
IHubContext<AudioHub> hubContext)
|
||
{
|
||
_audioProcessingService = audioProcessingService ?? throw new ArgumentNullException(nameof(audioProcessingService));
|
||
_configurationService = configurationService ?? throw new ArgumentNullException(nameof(configurationService));
|
||
_speechToTextService = speechToTextService ?? throw new ArgumentNullException(nameof(speechToTextService));
|
||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||
_hubContext = hubContext ?? throw new ArgumentNullException(nameof(hubContext));
|
||
|
||
// 订阅语音识别结果事件
|
||
_speechToTextService.ResultReceived += OnSpeechToTextResultReceived;
|
||
_audioProcessingService.SpeechToTextResultReceived += OnSpeechToTextResultReceived;
|
||
_audioProcessingService.AudioSavedToFile += OnAudioSavedToFile;
|
||
_configurationService.ConfigurationChanged += OnConfigurationChanged;
|
||
|
||
// 初始化清理计时器(如果尚未初始化)
|
||
if (_cleanupTimer == null)
|
||
{
|
||
_cleanupTimer = new Timer(CleanupOldProcessedPaths, null, TimeSpan.FromMinutes(30), TimeSpan.FromMinutes(30));
|
||
}
|
||
|
||
// 从文件加载预设句子
|
||
LoadPresetSentencesFromFile();
|
||
|
||
// 从文件加载监控文本队列
|
||
LoadMonitorTextQueueFromFile();
|
||
|
||
// 初始化显示文本定时器
|
||
InitializeDisplayTextTimer();
|
||
|
||
// 注册应用程序域卸载事件,以便在应用关闭时保存数据
|
||
AppDomain.CurrentDomain.ProcessExit += (sender, e) => SaveMonitorTextQueueToFile();
|
||
AppDomain.CurrentDomain.DomainUnload += (sender, e) => SaveMonitorTextQueueToFile();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 清理过期的处理记录
|
||
/// </summary>
|
||
private static void CleanupOldProcessedPaths(object state)
|
||
{
|
||
try
|
||
{
|
||
// 删除超过24小时的记录
|
||
var expireTime = DateTime.Now.AddHours(-24);
|
||
var keysToRemove = _globalProcessedPaths.Where(kvp => kvp.Value < expireTime)
|
||
.Select(kvp => kvp.Key)
|
||
.ToList();
|
||
|
||
foreach (var key in keysToRemove)
|
||
{
|
||
_globalProcessedPaths.TryRemove(key, out _);
|
||
}
|
||
}
|
||
catch
|
||
{
|
||
// 忽略清理过程中的错误
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 客户端连接时触发
|
||
/// </summary>
|
||
/// <returns>连接处理任务</returns>
|
||
public override async Task OnConnectedAsync()
|
||
{
|
||
var httpContext = Context.GetHttpContext();
|
||
var clientIp = httpContext?.Connection?.RemoteIpAddress?.ToString() ?? "Unknown";
|
||
var userAgent = httpContext?.Request.Headers["User-Agent"].ToString() ?? "Unknown";
|
||
|
||
_logger.LogInformation($"客户端连接: {Context.ConnectionId}, IP: {clientIp}");
|
||
|
||
// 等待客户端发送身份信息
|
||
await base.OnConnectedAsync();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 客户端断开连接时触发
|
||
/// </summary>
|
||
/// <param name="exception">异常信息</param>
|
||
/// <returns>断开连接处理任务</returns>
|
||
public override async Task OnDisconnectedAsync(Exception exception)
|
||
{
|
||
if (_clients.TryRemove(Context.ConnectionId, out var clientInfo))
|
||
{
|
||
_logger.LogInformation($"客户端断开连接: {Context.ConnectionId}, 类型: {clientInfo.ClientType}, 名称: {clientInfo.Name}");
|
||
|
||
// 如果是控制器客户端,结束当前音频流
|
||
if (clientInfo.ClientType == ClientType.Controller)
|
||
{
|
||
await _audioProcessingService.EndAudioStreamAsync(Context.ConnectionId);
|
||
await _speechToTextService.EndSessionAsync(Context.ConnectionId);
|
||
}
|
||
|
||
// 通知所有管理端客户端断开连接
|
||
await Clients.Group("webadmin").SendAsync("ClientDisconnected", clientInfo);
|
||
}
|
||
|
||
await base.OnDisconnectedAsync(exception);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 注册客户端
|
||
/// </summary>
|
||
/// <param name="clientType">客户端类型</param>
|
||
/// <param name="name">客户端名称</param>
|
||
/// <returns>注册处理任务</returns>
|
||
public async Task RegisterClient(ClientType clientType, string name)
|
||
{
|
||
var httpContext = Context.GetHttpContext();
|
||
var clientIp = httpContext?.Connection?.RemoteIpAddress?.ToString() ?? "Unknown";
|
||
var userAgent = httpContext?.Request.Headers["User-Agent"].ToString() ?? "Unknown";
|
||
|
||
var clientInfo = new ClientInfo
|
||
{
|
||
ClientId = Context.ConnectionId,
|
||
ClientType = clientType,
|
||
Name = name,
|
||
IpAddress = clientIp,
|
||
UserAgent = userAgent
|
||
};
|
||
|
||
if (_clients.TryAdd(Context.ConnectionId, clientInfo))
|
||
{
|
||
_logger.LogInformation($"客户端注册成功: {Context.ConnectionId}, 类型: {clientType}, 名称: {name}");
|
||
|
||
// 根据客户端类型加入不同的组
|
||
switch (clientType)
|
||
{
|
||
case ClientType.Controller:
|
||
await Groups.AddToGroupAsync(Context.ConnectionId, "controllers");
|
||
break;
|
||
case ClientType.WebAdmin:
|
||
await Groups.AddToGroupAsync(Context.ConnectionId, "webadmin");
|
||
|
||
// 发送当前配置给管理端
|
||
await Clients.Caller.SendAsync("ReceiveConfiguration", _configurationService.CurrentConfig);
|
||
|
||
// 发送客户端列表给管理端
|
||
await Clients.Caller.SendAsync("ClientList", _clients.Values);
|
||
break;
|
||
case ClientType.Monitor:
|
||
await Groups.AddToGroupAsync(Context.ConnectionId, "monitor");
|
||
break;
|
||
case ClientType.Display:
|
||
await Groups.AddToGroupAsync(Context.ConnectionId, "displays");
|
||
break;
|
||
}
|
||
|
||
// 通知所有管理端有新客户端连接
|
||
await Clients.Group("webadmin").SendAsync("NewClientConnected", clientInfo);
|
||
|
||
// 给客户端发送确认消息
|
||
await Clients.Caller.SendAsync("RegistrationConfirmed", clientInfo.ClientId);
|
||
}
|
||
else
|
||
{
|
||
_logger.LogWarning($"客户端注册失败: {Context.ConnectionId}");
|
||
await Clients.Caller.SendAsync("RegistrationFailed", "注册失败,连接ID已存在");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理开始音频流请求
|
||
/// </summary>
|
||
/// <param name="sampleRate">采样率</param>
|
||
/// <param name="channels">声道数</param>
|
||
/// <returns>处理任务</returns>
|
||
public async Task StartAudioStream(int sampleRate, int channels)
|
||
{
|
||
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
|
||
{
|
||
_logger.LogWarning($"未注册的客户端尝试开始音频流: {Context.ConnectionId}");
|
||
await Clients.Caller.SendAsync("Error", "请先注册客户端");
|
||
return;
|
||
}
|
||
|
||
if (clientInfo.ClientType != ClientType.Controller)
|
||
{
|
||
_logger.LogWarning($"非控制器客户端尝试开始音频流: {Context.ConnectionId}, 类型: {clientInfo.ClientType}");
|
||
await Clients.Caller.SendAsync("Error", "只有控制器客户端可以发送音频流");
|
||
return;
|
||
}
|
||
|
||
_logger.LogInformation($"开始音频流: {Context.ConnectionId}, 采样率: {sampleRate}, 声道: {channels}");
|
||
|
||
// 开始音频处理
|
||
await _audioProcessingService.StartAudioStreamAsync(Context.ConnectionId, sampleRate, channels);
|
||
|
||
// 开始语音识别会话
|
||
await _speechToTextService.StartSessionAsync(Context.ConnectionId, sampleRate);
|
||
|
||
// 更新客户端状态
|
||
clientInfo.IsAudioStreaming = true;
|
||
_clients[Context.ConnectionId] = clientInfo;
|
||
|
||
// 通知所有管理端开始接收音频流
|
||
await Clients.Group("webadmin").SendAsync("AudioStreamStarted", Context.ConnectionId, sampleRate, channels);
|
||
|
||
// 通知所有监听中的显示端有通话开始
|
||
await Clients.Group("monitor").SendAsync("CallStateChanged", true);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理结束音频流请求
|
||
/// </summary>
|
||
/// <returns>处理任务</returns>
|
||
public async Task EndAudioStream()
|
||
{
|
||
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
|
||
{
|
||
_logger.LogWarning($"未注册的客户端尝试结束音频流: {Context.ConnectionId}");
|
||
return;
|
||
}
|
||
|
||
_logger.LogInformation($"结束音频流: {Context.ConnectionId}");
|
||
|
||
// 结束音频处理
|
||
await _audioProcessingService.EndAudioStreamAsync(Context.ConnectionId);
|
||
// 结束语音识别会话
|
||
await _speechToTextService.EndSessionAsync(Context.ConnectionId);
|
||
|
||
var _speechsession = _speechToTextService.GetSessionStatus(Context.ConnectionId);
|
||
if (_speechsession.HasSavedText && !_speechsession.IsSentToDisplay)
|
||
{
|
||
|
||
AddRecognizedTextToDisplay(_speechsession.FinalText, true, Context.ConnectionId, _speechsession);
|
||
_speechsession.IsSentToDisplay = true;
|
||
_logger.LogInformation($"保存文件流: {JsonConvert.SerializeObject(_speechsession)}");
|
||
// 将结果发送给所有监听中的客户端
|
||
await _hubContext.Clients.Groups(new[] { "webadmin", "monitor" })
|
||
.SendAsync("ReceiveSpeechToEndTextResult", _speechsession.FinalText);
|
||
}
|
||
// 更新客户端状态
|
||
clientInfo.IsAudioStreaming = false;
|
||
_clients[Context.ConnectionId] = clientInfo;
|
||
|
||
//
|
||
// 通知所有管理端结束接收音频流
|
||
await Clients.Group("webadmin").SendAsync("AudioStreamEnded", Context.ConnectionId);
|
||
|
||
// 通知所有监听中的显示端通话结束
|
||
await Clients.Group("monitor").SendAsync("CallStateChanged", false);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理接收到的音频数据
|
||
/// </summary>
|
||
/// <param name="audioData">音频数据</param>
|
||
/// <returns>处理任务</returns>
|
||
public async Task ReceiveAudioData(byte[] audioData)
|
||
{
|
||
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
|
||
{
|
||
_logger.LogWarning($"未注册的客户端尝试发送音频数据: {Context.ConnectionId}");
|
||
var httpContext = Context.GetHttpContext();
|
||
var clientIp = httpContext?.Connection?.RemoteIpAddress?.ToString() ?? "Unknown";
|
||
var userAgent = httpContext?.Request.Headers["User-Agent"].ToString() ?? "Unknown";
|
||
|
||
clientInfo = new ClientInfo
|
||
{
|
||
ClientId = Context.ConnectionId,
|
||
ClientType = ClientType.Controller,
|
||
Name = "自动注册客户端",
|
||
IpAddress = clientIp,
|
||
UserAgent = userAgent
|
||
};
|
||
|
||
if (_clients.TryAdd(Context.ConnectionId, clientInfo))
|
||
{
|
||
}
|
||
//return;
|
||
}
|
||
|
||
if (clientInfo.ClientType != ClientType.Controller)
|
||
{
|
||
_logger.LogWarning($"非控制器客户端尝试发送音频数据: {Context.ConnectionId}, 类型: {clientInfo.ClientType}");
|
||
return;
|
||
}
|
||
|
||
// 处理音频数据
|
||
var config = _configurationService.CurrentConfig.Recording;
|
||
await _audioProcessingService.ProcessAudioDataAsync(audioData, config.SampleRate, config.Channels, Context.ConnectionId);
|
||
|
||
// 处理语音识别
|
||
if (_configurationService.CurrentConfig.Network.EnableSpeechToText)
|
||
{
|
||
await _speechToTextService.ProcessAudioAsync(audioData, Context.ConnectionId);
|
||
}
|
||
|
||
// 转发音频数据到管理端
|
||
if (_configurationService.CurrentConfig.Network.EnableAudioStreaming)
|
||
{
|
||
// await Clients.Group("webadmin").SendAsync("ReceiveAudioData", Context.ConnectionId, audioData);
|
||
|
||
// 转发音频到正在监听的显示端
|
||
var monitoringClients = _clients.Values
|
||
.Where(c => (c.ClientType == ClientType.Monitor || c.ClientType == ClientType.WebAdmin))
|
||
.Select(c => c.ClientId)
|
||
.ToList();
|
||
|
||
if (monitoringClients.Any())
|
||
{
|
||
try
|
||
{
|
||
byte[] dataToSend;
|
||
|
||
if (_configurationService.CurrentConfig.Network.EnableAudioNoiseReduction)
|
||
{
|
||
dataToSend = _audioProcessingService.ApplyNoiseReduction(audioData, config.SampleRate, config.Channels);
|
||
_logger.LogDebug($"转发音频数据到{monitoringClients.Count}个监听客户端,数据长度: {audioData.Length},降噪后长度:{dataToSend.Length}");
|
||
}
|
||
else
|
||
{
|
||
dataToSend = audioData;
|
||
_logger.LogDebug($"转发音频数据到{monitoringClients.Count}个监听客户端,数据长度: {audioData.Length}");
|
||
}
|
||
|
||
// 始终使用二进制格式发送数据,避免字符串转换
|
||
await Clients.Clients(monitoringClients).SendAsync("ReceiveAudioData", dataToSend);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError($"转发音频数据到监听端失败: {ex.Message}");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理更新配置请求
|
||
/// </summary>
|
||
/// <param name="configJson">配置JSON字符串</param>
|
||
/// <returns>处理任务</returns>
|
||
public async Task UpdateConfiguration(string configJson)
|
||
{
|
||
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
|
||
{
|
||
_logger.LogWarning($"未注册的客户端尝试更新配置: {Context.ConnectionId}");
|
||
await Clients.Caller.SendAsync("Error", "请先注册客户端");
|
||
return;
|
||
}
|
||
|
||
if (clientInfo.ClientType != ClientType.WebAdmin)
|
||
{
|
||
_logger.LogWarning($"非管理端客户端尝试更新配置: {Context.ConnectionId}, 类型: {clientInfo.ClientType}");
|
||
await Clients.Caller.SendAsync("Error", "只有管理端客户端可以更新配置");
|
||
return;
|
||
}
|
||
|
||
_logger.LogInformation($"更新配置: {Context.ConnectionId}");
|
||
|
||
// 更新配置
|
||
var success = _configurationService.UpdateConfigurationFromJson(configJson);
|
||
|
||
if (success)
|
||
{
|
||
// 配置更新成功事件会广播新配置
|
||
await Clients.Caller.SendAsync("ConfigurationUpdated", true, "配置更新成功");
|
||
}
|
||
else
|
||
{
|
||
await Clients.Caller.SendAsync("ConfigurationUpdated", false, "配置更新失败");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 处理获取录音列表请求
|
||
/// </summary>
|
||
/// <param name="count">获取数量</param>
|
||
/// <returns>处理任务</returns>
|
||
public async Task GetRecentRecordings(int count = 100)
|
||
{
|
||
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
|
||
{
|
||
_logger.LogWarning($"未注册的客户端尝试获取录音列表: {Context.ConnectionId}");
|
||
await Clients.Caller.SendAsync("Error", "请先注册客户端");
|
||
return;
|
||
}
|
||
|
||
if (clientInfo.ClientType != ClientType.WebAdmin && clientInfo.ClientType != ClientType.Monitor)
|
||
{
|
||
_logger.LogWarning($"非管理端或显示端客户端尝试获取录音列表: {Context.ConnectionId}, 类型: {clientInfo.ClientType}");
|
||
await Clients.Caller.SendAsync("Error", "只有管理端或显示端客户端可以获取录音列表");
|
||
return;
|
||
}
|
||
|
||
_logger.LogInformation($"获取录音列表: {Context.ConnectionId}, 数量: {count}");
|
||
|
||
// 获取录音列表
|
||
var recordings = _audioProcessingService.GetRecentRecordings(count);
|
||
|
||
// 发送录音列表
|
||
await Clients.Caller.SendAsync("RecentRecordings", recordings);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取最新配置
|
||
/// </summary>
|
||
/// <returns>配置JSON字符串</returns>
|
||
public async Task<string> GetLatestConfig()
|
||
{
|
||
try
|
||
{
|
||
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
|
||
{
|
||
_logger.LogWarning($"未注册的客户端尝试获取配置: {Context.ConnectionId}");
|
||
return string.Empty;
|
||
}
|
||
|
||
_logger.LogInformation($"客户端请求获取最新配置: {Context.ConnectionId}, 类型: {clientInfo.ClientType}");
|
||
|
||
// 读取config.json文件
|
||
string configPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config.json");
|
||
if (!File.Exists(configPath))
|
||
{
|
||
_logger.LogWarning($"配置文件不存在: {configPath}");
|
||
return JsonConvert.SerializeObject(_configurationService.CurrentConfig);
|
||
}
|
||
|
||
string configJson = await File.ReadAllTextAsync(configPath);
|
||
_logger.LogInformation($"已读取配置文件: {configPath}");
|
||
return configJson;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError($"获取配置时出错: {ex.Message}");
|
||
// 出错时返回当前内存中的配置
|
||
return JsonConvert.SerializeObject(_configurationService.CurrentConfig);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取监控文本列表
|
||
/// </summary>
|
||
/// <returns>监控文本列表</returns>
|
||
public async Task<List<DisplayText>> GetMonitorTextList()
|
||
{
|
||
var result = _monitorTextQueue.Select(kvp => kvp.Value).OrderBy(it => it.Timestamp).ToList();
|
||
|
||
// 转换文件路径为URL路径
|
||
foreach (var item in result)
|
||
{
|
||
ConvertFilePathsToUrls(item);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取显示文本列表
|
||
/// </summary>
|
||
/// <returns>显示文本列表</returns>
|
||
public async Task<List<DisplayText>> GetDisplayList()
|
||
{
|
||
var result = _displayTextQueue.Select(kvp => kvp.Value).Where(it => it.IsRealUser).OrderBy(it => it.Timestamp).ToList();
|
||
|
||
// 转换文件路径为URL路径
|
||
foreach (var item in result)
|
||
{
|
||
ConvertFilePathsToUrls(item);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将文件路径转换为URL路径
|
||
/// </summary>
|
||
/// <param name="displayText">要转换的显示文本对象</param>
|
||
private void ConvertFilePathsToUrls(DisplayText displayText)
|
||
{
|
||
// 转换录音文件路径
|
||
if (!string.IsNullOrEmpty(displayText.RecordingPath))
|
||
{
|
||
// 从完整路径中提取文件名
|
||
string fileName = Path.GetFileName(displayText.RecordingPath);
|
||
|
||
// 从路径中识别/recordings/目录
|
||
if (displayText.RecordingPath.Contains("recordings"))
|
||
{
|
||
int recordingsIndex = displayText.RecordingPath.IndexOf("recordings", StringComparison.OrdinalIgnoreCase);
|
||
string relativePath = displayText.RecordingPath.Substring(recordingsIndex);
|
||
displayText.RecordingPath = $"/{relativePath.Replace('\\', '/')}";
|
||
}
|
||
else
|
||
{
|
||
// 如果路径不包含recordings目录,则直接使用文件名
|
||
displayText.RecordingPath = $"/recordings/{fileName}";
|
||
}
|
||
}
|
||
|
||
// 转换文本文件路径
|
||
if (!string.IsNullOrEmpty(displayText.TextFilePath))
|
||
{
|
||
// 从完整路径中提取文件名
|
||
string fileName = Path.GetFileName(displayText.TextFilePath);
|
||
|
||
// 从路径中识别/texts/目录
|
||
if (displayText.TextFilePath.Contains("texts"))
|
||
{
|
||
int textsIndex = displayText.TextFilePath.IndexOf("texts", StringComparison.OrdinalIgnoreCase);
|
||
string relativePath = displayText.TextFilePath.Substring(textsIndex);
|
||
displayText.TextFilePath = $"/{relativePath.Replace('\\', '/')}";
|
||
}
|
||
else
|
||
{
|
||
// 如果路径不包含texts目录,则直接使用文件名
|
||
displayText.TextFilePath = $"/texts/{fileName}";
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 删除
|
||
/// </summary>
|
||
/// <returns></returns>
|
||
|
||
public async Task<bool> DelMonitorText(Guid id)
|
||
{
|
||
if (_monitorTextQueue.TryRemove(id, out _))
|
||
{
|
||
// 删除后保存到文件
|
||
SaveMonitorTextQueueToFile();
|
||
return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 添加文本
|
||
/// </summary>
|
||
/// <returns></returns>
|
||
|
||
public async Task<bool> AddDisplayList(string text)
|
||
{
|
||
var id = Guid.NewGuid();
|
||
_displayTextQueue.TryAdd(id, new DisplayText()
|
||
{
|
||
CompletedText = text,
|
||
IsProcessed = true,
|
||
Id = id,
|
||
IsRealUser = true,
|
||
RecognitionId = "",
|
||
Text = text,
|
||
Timestamp = DateTime.Now
|
||
});
|
||
return false;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 语音识别结果事件处理
|
||
/// </summary>
|
||
/// <param name="sender">发送者</param>
|
||
/// <param name="e">事件参数</param>
|
||
private async void OnSpeechToTextResultReceived(object sender, SpeechToTextResult e)
|
||
{
|
||
try
|
||
{
|
||
// 只处理最终结果,且不是仅用于显示的中间结果
|
||
if (e.IsFinal && !e.IsForDisplayOnly)
|
||
{
|
||
_logger.LogInformation($"接收到语音识别最终结果: {e.Text}");
|
||
|
||
// 发送频率限制:每秒最多发送3次最终识别结果
|
||
DateTime now = DateTime.Now;
|
||
TimeSpan elapsed = now - _lastFinalResultSentTime;
|
||
|
||
// 检查是否已进入新的时间窗口
|
||
if (elapsed >= _finalResultThrottleWindow)
|
||
{
|
||
// 重置计数器和时间窗口
|
||
_finalResultCounter = 0;
|
||
_lastFinalResultSentTime = now;
|
||
}
|
||
|
||
// 检查当前时间窗口内是否已达到发送上限
|
||
if (_finalResultCounter < _maxFinalResultsPerSecond)
|
||
{
|
||
// 将最终结果发送给所有监听中的客户端
|
||
await _hubContext.Clients.Groups(new[] { "webadmin", "monitor" })
|
||
.SendAsync("ReceiveSpeechToEndTextResult", e.Text);
|
||
|
||
// 增加计数器
|
||
_finalResultCounter++;
|
||
}
|
||
else
|
||
{
|
||
_logger.LogDebug($"已达到最终结果发送频率限制,跳过发送: {e.Text}");
|
||
}
|
||
}
|
||
else
|
||
{
|
||
// 发送频率限制:每秒最多发送3次实时识别结果
|
||
DateTime now = DateTime.Now;
|
||
TimeSpan elapsed = now - _lastRealTimeResultSentTime;
|
||
|
||
// 检查是否已进入新的时间窗口
|
||
if (elapsed >= _realTimeResultThrottleWindow)
|
||
{
|
||
// 重置计数器和时间窗口
|
||
_realTimeResultCounter = 0;
|
||
_lastRealTimeResultSentTime = now;
|
||
}
|
||
|
||
// 检查当前时间窗口内是否已达到发送上限
|
||
if (_realTimeResultCounter < _maxRealTimeResultsPerSecond)
|
||
{
|
||
// 发送实时识别结果给客户端
|
||
_logger.LogInformation($"接收到语音识别实时结果: {e.Text}");
|
||
await _hubContext.Clients.Groups(new[] { "webadmin", "monitor" })
|
||
.SendAsync("ReceiveSpeechToTextResult", e.Text);
|
||
|
||
// 增加计数器
|
||
_realTimeResultCounter++;
|
||
}
|
||
else
|
||
{
|
||
_logger.LogDebug($"已达到实时结果发送频率限制,跳过发送: {e.Text}");
|
||
}
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError($"处理语音识别结果事件时出错: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 添加识别文本到显示队列
|
||
/// </summary>
|
||
/// <param name="text">文本内容</param>
|
||
/// <param name="isRealUser">是否来自真实用户</param>
|
||
/// <param name="recognitionId">识别结果ID</param>
|
||
public void AddRecognizedTextToDisplay(string text, bool isRealUser = true, string recognitionId = null, SpeechRecognitionSession session = null)
|
||
{
|
||
if (string.IsNullOrWhiteSpace(text))
|
||
return;
|
||
|
||
var displayText = new DisplayText
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
Text = text,
|
||
Timestamp = DateTime.Now,
|
||
IsRealUser = isRealUser,
|
||
RecognitionId = recognitionId
|
||
};
|
||
if (session != null)
|
||
{
|
||
displayText.TextFilePath = session.TextFilePath;
|
||
displayText.RecordingPath = session.RecordingPath;
|
||
}
|
||
if (isRealUser)
|
||
{
|
||
if (_configurationService.CurrentConfig.DisplayType == 1)
|
||
{
|
||
_monitorTextQueue.TryAdd(displayText.Id, displayText);
|
||
// 添加到监控队列后保存文件
|
||
SaveMonitorTextQueueToFile();
|
||
}
|
||
else if (_configurationService.CurrentConfig.DisplayType == 2)
|
||
{
|
||
_aiTextQueue.TryAdd(displayText.Id, displayText);
|
||
}
|
||
else
|
||
{
|
||
_displayTextQueue.TryAdd(displayText.Id, displayText);
|
||
}
|
||
|
||
_lastRealUserSpeakTime = DateTime.Now;
|
||
_logger.LogInformation($"添加真实用户文本到显示队列: {text}");
|
||
}
|
||
else
|
||
{
|
||
_displayTextQueue.TryAdd(displayText.Id, displayText);
|
||
_logger.LogInformation($"添加预设文本到显示队列: {text}");
|
||
}
|
||
|
||
// 确保定时器已初始化
|
||
InitializeDisplayTextTimer();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化显示文本定时器
|
||
/// </summary>
|
||
private void InitializeDisplayTextTimer()
|
||
{
|
||
// 注意:不再需要实际启动定时器,因为已改为客户端主动请求模式
|
||
// 但保留此方法以保持代码结构兼容性
|
||
if (!_isDisplayTimerInitialized)
|
||
{
|
||
lock (_displayTimerLock)
|
||
{
|
||
if (!_isDisplayTimerInitialized)
|
||
{
|
||
// 不再需要实际启动定时器
|
||
// _displayTextTimer = new Timer(SendNextDisplayText, null, 0, Timeout.Infinite);
|
||
_isDisplayTimerInitialized = true;
|
||
_logger.LogInformation("显示文本系统已初始化(客户端主动获取模式)");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 注意:以下方法不再使用,由客户端主动获取替代,保留代码仅作参考
|
||
/// <summary>
|
||
/// 发送下一条显示文本(不再使用,由客户端主动获取替代)
|
||
/// </summary>
|
||
/// <param name="state">状态对象</param>
|
||
private async void SendNextDisplayText(object state)
|
||
{
|
||
// 这个方法不再使用,由客户端主动获取替代
|
||
// 保留原有逻辑仅作参考
|
||
|
||
try
|
||
{
|
||
// 获取随机等待时间(10-30秒)
|
||
int nextInterval = new Random().Next(10000, 30000);
|
||
|
||
// 设置下一次触发时间,此处设置为较长时间,避免频繁触发未使用的方法
|
||
_displayTextTimer?.Change(nextInterval, Timeout.Infinite);
|
||
|
||
_logger.LogWarning("使用了已废弃的发送显示文本方法,应改为客户端主动获取模式");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError($"发送显示文本失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 开始接收显示文本(兼容旧客户端,新客户端应使用GetNextDisplayText)
|
||
/// </summary>
|
||
public async Task StartReceivingDisplayText()
|
||
{
|
||
// 兼容旧版客户端的方法
|
||
_logger.LogWarning($"客户端 {Context.ConnectionId} 使用了已废弃的StartReceivingDisplayText方法,应改为GetNextDisplayText");
|
||
|
||
// 转发到新方法
|
||
await GetNextDisplayText();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取下一条要显示的文本(由客户端主动请求)
|
||
/// </summary>
|
||
/// <returns>处理任务</returns>
|
||
public async Task GetNextDisplayText()
|
||
{
|
||
try
|
||
{
|
||
// 检查客户端是否已注册
|
||
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
|
||
{
|
||
_logger.LogWarning($"未注册的客户端尝试获取显示文本: {Context.ConnectionId}");
|
||
await Clients.Caller.SendAsync("Error", "请先注册客户端");
|
||
return;
|
||
}
|
||
|
||
// 检查客户端是否为Display类型
|
||
if (clientInfo.ClientType != ClientType.Display)
|
||
{
|
||
_logger.LogWarning($"非显示端客户端尝试获取显示文本: {Context.ConnectionId}, 类型: {clientInfo.ClientType}");
|
||
await Clients.Caller.SendAsync("Error", "只有显示端客户端可以获取显示文本");
|
||
return;
|
||
}
|
||
|
||
_logger.LogInformation($"显示端请求获取下一条显示文本: {Context.ConnectionId}");
|
||
|
||
// 检查队列是否为空
|
||
if (_displayTextQueue.IsEmpty)
|
||
{
|
||
// 检查是否需要添加预设文本
|
||
if (DateTime.Now.Subtract(_lastRealUserSpeakTime).TotalSeconds > 30)
|
||
{
|
||
AddFakeTextToQueue();
|
||
}
|
||
else
|
||
{
|
||
_logger.LogInformation("显示文本队列为空,且不需要添加预设文本");
|
||
// 如果队列为空且不需要添加预设文本,返回空
|
||
return;
|
||
}
|
||
}
|
||
|
||
// 从队列中选择最高优先级的消息
|
||
var highestPriority = _displayTextQueue.OrderByDescending(kvp => kvp.Value.Priority)
|
||
.ThenBy(it => it.Value.Timestamp)
|
||
.FirstOrDefault();
|
||
|
||
// 检查是否获取到有效的消息
|
||
if (highestPriority.Key == Guid.Empty || highestPriority.Value == null)
|
||
{
|
||
_logger.LogWarning("无法从队列中获取有效的显示文本");
|
||
return;
|
||
}
|
||
|
||
// 在手动控屏模式下,只处理真实用户的消息
|
||
if (_manualScreenControlEnabled && !highestPriority.Value.IsRealUser)
|
||
{
|
||
_logger.LogInformation("当前为手动控屏模式,跳过非真实用户消息");
|
||
// 从队列中移除该消息,但不发送
|
||
if (_displayTextQueue.TryRemove(highestPriority.Key, out _))
|
||
{
|
||
// 返回空表示没有要显示的文本
|
||
await Clients.Caller.SendAsync("ReceiveDisplayText", "");
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 从队列中移除该消息
|
||
if (_displayTextQueue.TryRemove(highestPriority.Key, out var textToDisplay))
|
||
{
|
||
// 只发送给请求的客户端
|
||
await Clients.Caller.SendAsync("ReceiveDisplayText", textToDisplay.Text);
|
||
_logger.LogInformation($"已发送显示文本到客户端: {textToDisplay.Text} (来源: {(textToDisplay.IsRealUser ? "真实用户" : "预设文本")})");
|
||
|
||
// 如果是真实用户的发言,持久化保存到文件
|
||
if (textToDisplay.IsRealUser)
|
||
{
|
||
SaveRealUserDisplayToFile(textToDisplay);
|
||
}
|
||
}
|
||
else
|
||
{
|
||
_logger.LogWarning($"无法从队列中移除显示文本: {highestPriority.Key}");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError($"获取显示文本失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 添加预设文本到队列
|
||
/// </summary>
|
||
private void AddFakeTextToQueue()
|
||
{
|
||
try
|
||
{
|
||
// 确保预设句子列表不为空
|
||
if (_presetSentences.Count == 0)
|
||
{
|
||
LoadPresetSentencesFromFile();
|
||
|
||
// 如果加载后仍然为空,使用默认句子
|
||
if (_presetSentences.Count == 0)
|
||
{
|
||
_presetSentences.Add("记得每到夏天傍晚,您就摇着蒲扇坐在藤椅里,把切好的西瓜最甜那块硬塞给我。");
|
||
_presetSentences.Add("时光匆匆流逝,思念却越来越深。");
|
||
}
|
||
}
|
||
_presetSentences.OrderBy(it => Guid.NewGuid()).ToList().ForEach(item =>
|
||
{
|
||
var displayText = new DisplayText
|
||
{
|
||
Id = Guid.NewGuid(),
|
||
Text = item,
|
||
Timestamp = DateTime.Now,
|
||
IsRealUser = false,
|
||
RecognitionId = ""
|
||
};
|
||
_displayTextQueue.TryAdd(displayText.Id, displayText);
|
||
});
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError($"添加预设文本失败: {ex.Message}");
|
||
// 使用一个默认句子防止程序崩溃
|
||
AddRecognizedTextToDisplay("时光匆匆流逝,思念却越来越深。", false);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从文件加载预设句子
|
||
/// </summary>
|
||
private void LoadPresetSentencesFromFile()
|
||
{
|
||
try
|
||
{
|
||
_presetSentences.Clear();
|
||
|
||
// 检查文件是否存在
|
||
if (!File.Exists(_sentencesFilePath))
|
||
{
|
||
_logger.LogWarning($"预设句子文件不存在: {_sentencesFilePath},将创建默认文件");
|
||
|
||
// 创建目录(如果不存在)
|
||
Directory.CreateDirectory(Path.GetDirectoryName(_sentencesFilePath));
|
||
|
||
// 写入默认的预设句子
|
||
File.WriteAllLines(_sentencesFilePath, new string[] {
|
||
|
||
});
|
||
}
|
||
|
||
// 读取文件中的每一行作为一个预设句子
|
||
string[] lines = File.ReadAllLines(_sentencesFilePath);
|
||
foreach (string line in lines)
|
||
{
|
||
// 忽略空行
|
||
if (!string.IsNullOrWhiteSpace(line))
|
||
{
|
||
_presetSentences.Add(line.Trim());
|
||
}
|
||
}
|
||
_presetSentences = _presetSentences.OrderBy(x => Guid.NewGuid()).ToList();
|
||
_logger.LogInformation($"成功从文件加载预设句子: {_presetSentences.Count} 条");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError($"加载预设句子失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从文件加载监控文本队列
|
||
/// </summary>
|
||
private void LoadMonitorTextQueueFromFile()
|
||
{
|
||
try
|
||
{
|
||
if (!File.Exists(_monitorTextQueueFilePath))
|
||
{
|
||
File.Create(_monitorTextQueueFilePath).Close();
|
||
_logger.LogInformation($"监控文本队列文件不存在: {_monitorTextQueueFilePath},手动创建");
|
||
//return;
|
||
}
|
||
|
||
string json = File.ReadAllText(_monitorTextQueueFilePath);
|
||
if (string.IsNullOrEmpty(json))
|
||
{
|
||
_logger.LogWarning("监控文本队列文件内容为空");
|
||
return;
|
||
}
|
||
|
||
var items = JsonConvert.DeserializeObject<List<DisplayText>>(json);
|
||
if (items == null || !items.Any())
|
||
{
|
||
_logger.LogWarning("没有从文件中读取到监控文本队列项");
|
||
return;
|
||
}
|
||
|
||
// 清空现有队列,并添加从文件加载的项
|
||
_monitorTextQueue.Clear();
|
||
foreach (var item in items)
|
||
{
|
||
_monitorTextQueue.TryAdd(item.Id, item);
|
||
}
|
||
|
||
_logger.LogInformation($"成功从文件加载监控文本队列: {items.Count} 项");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError($"加载监控文本队列失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 保存监控文本队列到文件
|
||
/// </summary>
|
||
private void SaveMonitorTextQueueToFile()
|
||
{
|
||
try
|
||
{
|
||
if (_monitorTextQueue.IsEmpty)
|
||
{
|
||
_logger.LogInformation("监控文本队列为空,无需保存");
|
||
// 如果队列为空但文件存在,删除文件
|
||
if (File.Exists(_monitorTextQueueFilePath))
|
||
{
|
||
File.Delete(_monitorTextQueueFilePath);
|
||
_logger.LogInformation($"已删除空的监控文本队列文件: {_monitorTextQueueFilePath}");
|
||
}
|
||
return;
|
||
}
|
||
|
||
var items = _monitorTextQueue.Values.ToList();
|
||
string json = JsonConvert.SerializeObject(items, Formatting.Indented);
|
||
|
||
File.WriteAllText(_monitorTextQueueFilePath, json);
|
||
_logger.LogInformation($"成功保存监控文本队列到文件: {items.Count} 项");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError($"保存监控文本队列失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前音频传输设置
|
||
/// </summary>
|
||
/// <returns>是否启用音频传输</returns>
|
||
public async Task<bool> GetAudioStreamingSetting()
|
||
{
|
||
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
|
||
{
|
||
_logger.LogWarning($"未注册的客户端尝试获取音频传输设置: {Context.ConnectionId}");
|
||
throw new HubException("请先注册客户端");
|
||
}
|
||
|
||
_logger.LogInformation($"客户端获取当前音频传输设置: {Context.ConnectionId}, 类型: {clientInfo.ClientType}");
|
||
return _configurationService.CurrentConfig.Network.EnableAudioStreaming;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新音频传输设置
|
||
/// </summary>
|
||
/// <param name="enabled">是否启用音频传输</param>
|
||
/// <returns>处理任务</returns>
|
||
public async Task UpdateAudioStreaming(bool enabled)
|
||
{
|
||
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
|
||
{
|
||
_logger.LogWarning($"未注册的客户端尝试更新音频传输设置: {Context.ConnectionId}");
|
||
await Clients.Caller.SendAsync("Error", "请先注册客户端");
|
||
return;
|
||
}
|
||
|
||
if (clientInfo.ClientType != ClientType.Monitor && clientInfo.ClientType != ClientType.WebAdmin)
|
||
{
|
||
_logger.LogWarning($"非监控或管理端客户端尝试更新音频传输设置: {Context.ConnectionId}, 类型: {clientInfo.ClientType}");
|
||
await Clients.Caller.SendAsync("Error", "只有监控或管理端客户端可以更新音频传输设置");
|
||
return;
|
||
}
|
||
|
||
_logger.LogInformation($"更新音频传输设置: {Context.ConnectionId}, 新设置: {(enabled ? "开启" : "关闭")}");
|
||
|
||
// 更新配置
|
||
_configurationService.CurrentConfig.Network.EnableAudioStreaming = enabled;
|
||
|
||
// 通知其他客户端音频传输设置已更改
|
||
await Clients.Groups(new[] { "monitor", "webadmin" }).SendAsync("AudioStreamingChanged", enabled);
|
||
|
||
// 保存配置到文件
|
||
try
|
||
{
|
||
var success = _configurationService.SaveConfiguration();
|
||
if (success)
|
||
{
|
||
await Clients.Caller.SendAsync("AudioStreamingUpdated", true, $"音频传输设置已更新为: {(enabled ? "开启" : "关闭")}");
|
||
}
|
||
else
|
||
{
|
||
_logger.LogError("保存配置失败");
|
||
await Clients.Caller.SendAsync("AudioStreamingUpdated", false, "更新音频传输设置成功,但保存配置失败");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError($"保存配置到文件时出错: {ex.Message}");
|
||
await Clients.Caller.SendAsync("AudioStreamingUpdated", false, "更新音频传输设置成功,但保存配置失败");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 释放资源
|
||
/// </summary>
|
||
/// <param name="disposing">是否正在处理Dispose</param>
|
||
protected virtual void Dispose(bool disposing)
|
||
{
|
||
if (!_disposed)
|
||
{
|
||
if (disposing)
|
||
{
|
||
// 保存监控文本队列到文件
|
||
SaveMonitorTextQueueToFile();
|
||
|
||
// 取消订阅所有事件
|
||
_speechToTextService.ResultReceived -= OnSpeechToTextResultReceived;
|
||
_audioProcessingService.SpeechToTextResultReceived -= OnSpeechToTextResultReceived;
|
||
_audioProcessingService.AudioSavedToFile -= OnAudioSavedToFile;
|
||
_configurationService.ConfigurationChanged -= OnConfigurationChanged;
|
||
|
||
_logger.LogDebug("已取消订阅所有事件");
|
||
}
|
||
|
||
_disposed = true;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 释放资源
|
||
/// </summary>
|
||
public new void Dispose()
|
||
{
|
||
Dispose(true);
|
||
base.Dispose();
|
||
GC.SuppressFinalize(this);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 配置变更事件处理
|
||
/// </summary>
|
||
/// <param name="sender">发送者</param>
|
||
/// <param name="e">配置</param>
|
||
private async void OnConfigurationChanged(object sender, PhoneBoothConfig e)
|
||
{
|
||
try
|
||
{
|
||
_logger.LogInformation("配置已更新");
|
||
|
||
// 使用_hubContext替代Clients
|
||
await _hubContext.Clients.Group("controllers").SendAsync("ReceiveConfiguration", e);
|
||
await _hubContext.Clients.Group("webadmin").SendAsync("ReceiveConfiguration", e);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError($"处理配置变更事件时出错: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 音频文件保存事件处理
|
||
/// </summary>
|
||
/// <param name="sender">发送者</param>
|
||
/// <param name="e">文件路径</param>
|
||
private async void OnAudioSavedToFile(object sender, string e)
|
||
{
|
||
try
|
||
{
|
||
// 快速检查路径是否已处理过,如果已处理过则直接返回
|
||
if (string.IsNullOrEmpty(e) || _globalProcessedPaths.ContainsKey(e))
|
||
{
|
||
return;
|
||
}
|
||
|
||
// 记录处理时间
|
||
_globalProcessedPaths[e] = DateTime.Now;
|
||
|
||
_logger.LogInformation($"音频保存到文件: {e}");
|
||
|
||
// 设置语音识别服务的录音文件路径
|
||
if (sender is IAudioProcessingService audioService)
|
||
{
|
||
// 查找对应的会话ID
|
||
var clientId = _clients.Keys.FirstOrDefault(id => audioService.GetRecordingFilePath(id) == e);
|
||
if (!string.IsNullOrEmpty(clientId))
|
||
{
|
||
// 生成会话文件路径的唯一标识符
|
||
string key = $"{clientId}:{e}";
|
||
|
||
// 添加到本地实例缓存 - 为双重保险
|
||
if (!_processedRecordingPaths.Contains(key))
|
||
{
|
||
_processedRecordingPaths.Add(key);
|
||
|
||
// 设置语音识别服务的录音文件路径
|
||
if (_speechToTextService is SpeechToTextService speechService)
|
||
{
|
||
_logger.LogInformation($"设置会话 {clientId} 的录音文件路径: {e}");
|
||
speechService.SetSessionRecordingPath(clientId, e);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// 使用_hubContext替代Clients
|
||
await _hubContext.Clients.Group("webadmin").SendAsync("AudioSavedToFile", e);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError($"处理音频保存事件时出错: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 开始监听音频
|
||
/// </summary>
|
||
/// <returns>处理任务</returns>
|
||
public async Task StartMonitoringAudio()
|
||
{
|
||
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
|
||
{
|
||
_logger.LogWarning($"未注册的客户端尝试开始监听音频: {Context.ConnectionId}");
|
||
await Clients.Caller.SendAsync("Error", "请先注册客户端");
|
||
return;
|
||
}
|
||
|
||
if (clientInfo.ClientType != ClientType.Monitor && clientInfo.ClientType != ClientType.WebAdmin)
|
||
{
|
||
_logger.LogWarning($"非显示端或管理端客户端尝试开始监听音频: {Context.ConnectionId}, 类型: {clientInfo.ClientType}");
|
||
await Clients.Caller.SendAsync("Error", "只有显示端或管理端客户端可以监听音频");
|
||
return;
|
||
}
|
||
|
||
_logger.LogInformation($"开始监听音频: {Context.ConnectionId}");
|
||
|
||
// 将客户端标记为监听状态
|
||
clientInfo.IsMonitoring = true;
|
||
_clients[Context.ConnectionId] = clientInfo;
|
||
|
||
// 如果有正在进行的通话,通知客户端
|
||
var activeControllers = _clients.Values.Where(c => c.ClientType == ClientType.Controller && c.IsAudioStreaming).ToList();
|
||
if (activeControllers.Any())
|
||
{
|
||
await Clients.Caller.SendAsync("CallStateChanged", true);
|
||
}
|
||
else
|
||
{
|
||
await Clients.Caller.SendAsync("CallStateChanged", false);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 停止监听音频
|
||
/// </summary>
|
||
/// <returns>处理任务</returns>
|
||
public async Task StopMonitoringAudio()
|
||
{
|
||
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
|
||
{
|
||
_logger.LogWarning($"未注册的客户端尝试停止监听音频: {Context.ConnectionId}");
|
||
return;
|
||
}
|
||
|
||
_logger.LogInformation($"停止监听音频: {Context.ConnectionId}");
|
||
|
||
// 将客户端标记为非监听状态
|
||
clientInfo.IsMonitoring = false;
|
||
_clients[Context.ConnectionId] = clientInfo;
|
||
|
||
await Clients.Caller.SendAsync("CallStateChanged", false);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从监控端发送消息
|
||
/// </summary>
|
||
/// <param name="sender">发送者姓名</param>
|
||
/// <param name="message">消息内容</param>
|
||
/// <returns>处理任务</returns>
|
||
public async Task SendMonitorMessage(string sender, string message)
|
||
{
|
||
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
|
||
{
|
||
_logger.LogWarning($"未注册的客户端尝试发送消息: {Context.ConnectionId}");
|
||
await Clients.Caller.SendAsync("Error", "请先注册客户端");
|
||
return;
|
||
}
|
||
|
||
if (clientInfo.ClientType != ClientType.Monitor && clientInfo.ClientType != ClientType.WebAdmin)
|
||
{
|
||
_logger.LogWarning($"非显示端或管理端客户端尝试发送消息: {Context.ConnectionId}, 类型: {clientInfo.ClientType}");
|
||
await Clients.Caller.SendAsync("Error", "只有显示端或管理端可以发送消息");
|
||
return;
|
||
}
|
||
|
||
_logger.LogInformation($"发送消息: {Context.ConnectionId}, 发送者: {sender}, 内容: {message}");
|
||
|
||
// 广播消息给所有显示端和管理端
|
||
await Clients.Groups(new[] { "displays", "webadmin", "monitor" }).SendAsync("ReceiveMessage", sender, message);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新显示模式类型
|
||
/// </summary>
|
||
/// <param name="displayType">显示类型:0表示识别立即显示,1表示手动显示</param>
|
||
/// <returns>处理任务</returns>
|
||
public async Task UpdateDisplayType(int displayType)
|
||
{
|
||
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
|
||
{
|
||
_logger.LogWarning($"未注册的客户端尝试更新显示模式: {Context.ConnectionId}");
|
||
await Clients.Caller.SendAsync("Error", "请先注册客户端");
|
||
return;
|
||
}
|
||
|
||
if (clientInfo.ClientType != ClientType.Monitor && clientInfo.ClientType != ClientType.WebAdmin)
|
||
{
|
||
_logger.LogWarning($"非监控或管理端客户端尝试更新显示模式: {Context.ConnectionId}, 类型: {clientInfo.ClientType}");
|
||
await Clients.Caller.SendAsync("Error", "只有监控或管理端客户端可以更新显示模式");
|
||
return;
|
||
}
|
||
|
||
if (displayType < 0 || displayType > 2)
|
||
{
|
||
_logger.LogWarning($"客户端尝试设置无效的显示模式: {Context.ConnectionId}, 模式: {displayType}");
|
||
await Clients.Caller.SendAsync("Error", "无效的显示模式值");
|
||
return;
|
||
}
|
||
|
||
_logger.LogInformation($"更新显示模式: {Context.ConnectionId}, 新模式: {displayType}");
|
||
|
||
// 更新配置
|
||
_configurationService.CurrentConfig.DisplayType = displayType;
|
||
|
||
// 通知其他客户端显示模式已更改
|
||
await Clients.Groups(new[] { "monitor", "webadmin" }).SendAsync("DisplayTypeChanged", displayType);
|
||
|
||
// 保存配置到文件
|
||
try
|
||
{
|
||
var success = _configurationService.SaveConfiguration();
|
||
if (success)
|
||
{
|
||
await Clients.Caller.SendAsync("DisplayTypeUpdated", true, $"显示模式已更新为: {displayType}");
|
||
}
|
||
else
|
||
{
|
||
_logger.LogError("保存配置失败");
|
||
await Clients.Caller.SendAsync("DisplayTypeUpdated", false, "更新显示模式成功,但保存配置失败");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError($"保存配置到文件时出错: {ex.Message}");
|
||
await Clients.Caller.SendAsync("DisplayTypeUpdated", false, "更新显示模式成功,但保存配置失败");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前显示模式
|
||
/// </summary>
|
||
/// <returns>当前显示模式</returns>
|
||
public async Task<int> GetDisplayType()
|
||
{
|
||
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
|
||
{
|
||
_logger.LogWarning($"未注册的客户端尝试获取显示模式: {Context.ConnectionId}");
|
||
throw new HubException("请先注册客户端");
|
||
}
|
||
|
||
_logger.LogInformation($"客户端获取当前显示模式: {Context.ConnectionId}, 类型: {clientInfo.ClientType}");
|
||
return _configurationService.CurrentConfig.DisplayType;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取显示配置
|
||
/// </summary>
|
||
/// <returns>显示配置的JSON字符串</returns>
|
||
public string GetDisplayConfig()
|
||
{
|
||
lock (_displayConfigLock)
|
||
{
|
||
return _displayConfig;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 保存显示配置
|
||
/// </summary>
|
||
/// <param name="configJson">配置的JSON字符串</param>
|
||
/// <returns>是否保存成功</returns>
|
||
public async Task<bool> SaveDisplayConfig(string configJson)
|
||
{
|
||
try
|
||
{
|
||
// 尝试解析JSON以验证格式
|
||
JsonDocument.Parse(configJson);
|
||
|
||
// 保存到文件
|
||
await File.WriteAllTextAsync(DisplayConfigPath, configJson);
|
||
|
||
// 更新内存中的配置
|
||
lock (_displayConfigLock)
|
||
{
|
||
_displayConfig = configJson;
|
||
}
|
||
|
||
// 通知所有显示客户端
|
||
await NotifyDisplayConfig();
|
||
|
||
return true;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
Console.WriteLine($"保存显示配置出错: {ex.Message}");
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 通知所有显示客户端更新配置
|
||
/// </summary>
|
||
private async Task NotifyDisplayConfig()
|
||
{
|
||
string configJson;
|
||
lock (_displayConfigLock)
|
||
{
|
||
configJson = _displayConfig;
|
||
}
|
||
|
||
List<string> displayClientIds = new List<string>();
|
||
lock (_clients)
|
||
{
|
||
displayClientIds = _clients
|
||
.Where(c => c.Value.ClientType.ToString() == "Display")
|
||
.Select(c => c.Key)
|
||
.ToList();
|
||
}
|
||
|
||
foreach (var clientId in displayClientIds)
|
||
{
|
||
await Clients.Client(clientId).SendAsync("ReceiveDisplayConfig", configJson);
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 保存真实用户显示文本到文件
|
||
/// </summary>
|
||
/// <param name="displayText">要保存的显示文本对象</param>
|
||
private void SaveRealUserDisplayToFile(DisplayText displayText)
|
||
{
|
||
try
|
||
{
|
||
// 确保目录存在
|
||
string directory = Path.GetDirectoryName(_realUserDisplayLogsPath);
|
||
if (!Directory.Exists(directory))
|
||
{
|
||
Directory.CreateDirectory(directory);
|
||
}
|
||
|
||
// 使用简单的序列化方式,将对象转换为单行文本
|
||
string entry = JsonConvert.SerializeObject(new RealUserDisplayRecord
|
||
{
|
||
Text = displayText.Text,
|
||
Timestamp = displayText.Timestamp.ToString("yyyy-MM-dd HH:mm:ss"),
|
||
});
|
||
|
||
// 使用AppendAllText确保追加到文件末尾,不覆盖原有内容
|
||
File.AppendAllText(_realUserDisplayLogsPath, entry + Environment.NewLine);
|
||
|
||
_logger.LogInformation($"已保存真实用户显示文本到文件: {displayText.Text}");
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError($"保存真实用户显示文本失败: {ex.Message}");
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取当前控屏设置
|
||
/// </summary>
|
||
/// <returns>是否为手动控屏模式</returns>
|
||
public async Task<bool> GetScreenControlSetting()
|
||
{
|
||
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
|
||
{
|
||
_logger.LogWarning($"未注册的客户端尝试获取控屏设置: {Context.ConnectionId}");
|
||
throw new HubException("请先注册客户端");
|
||
}
|
||
|
||
_logger.LogInformation($"客户端获取当前控屏设置: {Context.ConnectionId}, 类型: {clientInfo.ClientType}");
|
||
return _manualScreenControlEnabled;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 更新控屏设置
|
||
/// </summary>
|
||
/// <param name="isManual">是否为手动控屏模式</param>
|
||
/// <returns>更新结果</returns>
|
||
public async Task<bool> UpdateScreenControlSetting(bool isManual)
|
||
{
|
||
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
|
||
{
|
||
_logger.LogWarning($"未注册的客户端尝试更新控屏设置: {Context.ConnectionId}");
|
||
throw new HubException("请先注册客户端");
|
||
}
|
||
|
||
if (clientInfo.ClientType != ClientType.Monitor && clientInfo.ClientType != ClientType.WebAdmin)
|
||
{
|
||
_logger.LogWarning($"非监控端或管理端尝试更新控屏设置: {Context.ConnectionId}, 类型: {clientInfo.ClientType}");
|
||
throw new HubException("只有监控端或管理端可以更新控屏设置");
|
||
}
|
||
|
||
// 设置模式
|
||
_manualScreenControlEnabled = isManual;
|
||
|
||
// 保存设置
|
||
bool success = SaveScreenControlSetting();
|
||
|
||
if (success)
|
||
{
|
||
// 通知所有监控客户端更新设置
|
||
await Clients.Group("monitor").SendAsync("ScreenControlSettingChanged", isManual);
|
||
|
||
_logger.LogInformation($"控屏设置已更新为: {(isManual ? "手动" : "自动")}");
|
||
}
|
||
|
||
return success;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取真实用户聊天记录
|
||
/// </summary>
|
||
/// <returns>用户聊天记录列表</returns>
|
||
public async Task<List<RealUserDisplayRecord>> GetRealUserChatRecords()
|
||
{
|
||
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
|
||
{
|
||
_logger.LogWarning($"未注册的客户端尝试获取真实用户聊天记录: {Context.ConnectionId}");
|
||
throw new HubException("请先注册客户端");
|
||
}
|
||
|
||
if (clientInfo.ClientType != ClientType.WebAdmin)
|
||
{
|
||
_logger.LogWarning($"非管理端尝试获取真实用户聊天记录: {Context.ConnectionId}, 类型: {clientInfo.ClientType}");
|
||
throw new HubException("只有管理端可以获取真实用户聊天记录");
|
||
}
|
||
|
||
var records = new List<RealUserDisplayRecord>();
|
||
|
||
try
|
||
{
|
||
if (File.Exists(_realUserDisplayLogsPath))
|
||
{
|
||
var lines = await File.ReadAllLinesAsync(_realUserDisplayLogsPath);
|
||
foreach (var line in lines)
|
||
{
|
||
try
|
||
{
|
||
if (!string.IsNullOrWhiteSpace(line))
|
||
{
|
||
var record = JsonConvert.DeserializeObject<RealUserDisplayRecord>(line);
|
||
records.Add(record);
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError($"解析真实用户聊天记录行失败: {ex.Message}, 行内容: {line}");
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
_logger.LogWarning($"真实用户聊天记录文件不存在: {_realUserDisplayLogsPath}");
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogError($"获取真实用户聊天记录失败: {ex.Message}");
|
||
throw new HubException($"获取真实用户聊天记录失败: {ex.Message}");
|
||
}
|
||
|
||
return records;
|
||
}
|
||
}
|
||
} |