diff --git a/ShengShengBuXi/Hubs/AudioHub.cs b/ShengShengBuXi/Hubs/AudioHub.cs index 4d9f27c..8bfc91a 100644 --- a/ShengShengBuXi/Hubs/AudioHub.cs +++ b/ShengShengBuXi/Hubs/AudioHub.cs @@ -73,35 +73,23 @@ namespace ShengShengBuXi.Hubs /// /// 监控文本队列的持久化文件路径 /// - private static readonly string _monitorTextQueueFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "monitor_text_queue.json"); - // 预设句子 - private static readonly string[] _presetSentences = new string[] - { - "记得每到夏天傍晚,您就摇着蒲扇坐在藤椅里,把切好的西瓜最甜那块硬塞给我,自己却啃着靠近皮的白瓤,还笑着说:外公就爱这口,清爽。", - "女儿啊 花开了 你否到了吗?", - "外公,你在那边过的还好么大家都很想称,记得常回家看看", - "外公,今天窗台上的茉莉开了,白盈盈的,就像以前您总别在中山装口袋上的那朵。", - "外公,巷口那家老茶馆拆了,您最爱坐的靠窗位置再也找不到了,就像再也找不到您一样。", - "整理旧物时发现您用红绳缠好的象棋,每一处磨损都藏着您教我'马走日'时手心的温度。", - "今早闻到槐花香突然站住——您总说这味道像极了老家后山的夏天,现在我才懂得什么叫'睹物思人'。", - "菜场看见卖菱角的老伯,想起您总把最嫩的剥好放我碗里,自己却嚼着发苦的老根。", - "暴雨天膝盖又疼了吧?记得您总在这时候熬姜汤,说'老寒腿最懂天气预报'。", - "您养的那盆君子兰今年抽了七支花箭,比您走那年还多两枝,定是替您来看我的。", - "小满那天不自觉煮了两人份的腊肉饭,盛完才想起再没人把肥肉挑走给我留精瘦的。", - "儿童节路过小学,梧桐树下空荡荡的——再没有举着冰糖葫芦等我的驼背身影了。", - "冬至包饺子时手一抖,捏出您教的麦穗花边,滚水冒的蒸汽突然就迷了眼睛。", - "昨夜梦见您穿着洗白的蓝布衫,在晒谷场对我笑,醒来枕巾湿了半边。", - "蝉鸣最响的午后,恍惚听见竹椅吱呀声,转头却只看见墙上相框里的您。", - "您走后,再没人把枇杷核仔细包进手帕,笑着说'留着明年给囡囡种'。", - "整理遗物时数出38张火车票,全是往返我读书城市的,票根都磨出了毛边。", - "清明雨把墓碑冲洗得发亮,就像您当年总把搪瓷缸擦得能照见人影。", - "今天教女儿念'慈母手中线',她突然问:'太外公的诗集能读给我听吗?'", - "翻到您留下的老照片,那件洗得发白的中山装上还别着茉莉花,仿佛还能闻到淡淡的清香。", - "傍晚的蝉鸣声里,总错觉能听到您哼着那首走了调的小曲,在巷子口唤我回家吃饭。", - "您种的葡萄藤今年结了好多串,可再没有人像您那样,把最紫的摘下来悄悄塞进我口袋。", - "下雨天膝盖隐隐作痛时,总会想起您泡的姜茶,热气氤氲中您笑着说'老了才知道疼'。", - "整理书房时,发现您用毛笔在旧日历背面写的家训,墨迹晕染处都是您颤抖的手印。" - }; + private static readonly string _monitorTextQueueFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config/monitor_text_queue.json"); + /// + /// 预设句子文件路径 + /// + private static readonly string _sentencesFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config/sentences.txt"); + /// + /// 预设句子列表 + /// + private static List _presetSentences = new List(); + /// + /// 已显示过的预设句子队列及显示时间,用于防止短时间内重复显示 + /// + private static readonly Queue<(string Text, DateTime DisplayTime)> _recentlyDisplayedSentences = new Queue<(string, DateTime)>(); + /// + /// 防止重复显示的句子数量 + /// + private static readonly int _sentenceHistoryLimit = 20; /// /// 初始化音频Hub @@ -136,12 +124,15 @@ namespace ShengShengBuXi.Hubs _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(); @@ -402,7 +393,7 @@ namespace ShengShengBuXi.Hubs // 转发音频数据到管理端 if (_configurationService.CurrentConfig.Network.EnableAudioStreaming) { - // await Clients.Group("webadmin").SendAsync("ReceiveAudioData", Context.ConnectionId, audioData); + // await Clients.Group("webadmin").SendAsync("ReceiveAudioData", Context.ConnectionId, audioData); // 转发音频到正在监听的显示端 var monitoringClients = _clients.Values @@ -536,13 +527,13 @@ namespace ShengShengBuXi.Hubs public async Task> GetMonitorTextList() { var result = _monitorTextQueue.Select(kvp => kvp.Value).OrderBy(it => it.Timestamp).ToList(); - + // 转换文件路径为URL路径 foreach (var item in result) { ConvertFilePathsToUrls(item); } - + return result; } @@ -552,14 +543,14 @@ namespace ShengShengBuXi.Hubs /// 显示文本列表 public async Task> GetDisplayList() { - var result = _displayTextQueue.Select(kvp => kvp.Value).Where(it=>it.IsRealUser).OrderBy(it => it.Timestamp).ToList(); - + 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; } @@ -574,7 +565,7 @@ namespace ShengShengBuXi.Hubs { // 从完整路径中提取文件名 string fileName = Path.GetFileName(displayText.RecordingPath); - + // 从路径中识别/recordings/目录 if (displayText.RecordingPath.Contains("recordings")) { @@ -588,13 +579,13 @@ namespace ShengShengBuXi.Hubs displayText.RecordingPath = $"/recordings/{fileName}"; } } - + // 转换文本文件路径 if (!string.IsNullOrEmpty(displayText.TextFilePath)) { // 从完整路径中提取文件名 string fileName = Path.GetFileName(displayText.TextFilePath); - + // 从路径中识别/texts/目录 if (displayText.TextFilePath.Contains("texts")) { @@ -815,40 +806,311 @@ namespace ShengShengBuXi.Hubs /// private void AddFakeTextToQueue() { - // 从预设句子中随机选择一条 - string randomText = _presetSentences[new Random().Next(_presetSentences.Length)]; - AddRecognizedTextToDisplay(randomText, false); + try + { + // 确保预设句子列表不为空 + if (_presetSentences.Count == 0) + { + LoadPresetSentencesFromFile(); + + // 如果加载后仍然为空,使用默认句子 + if (_presetSentences.Count == 0) + { + _presetSentences.Add("记得每到夏天傍晚,您就摇着蒲扇坐在藤椅里,把切好的西瓜最甜那块硬塞给我。"); + _presetSentences.Add("时光匆匆流逝,思念却越来越深。"); + } + } + + // 清理超过24小时的历史记录 + while (_recentlyDisplayedSentences.Count > 0 && DateTime.Now.Subtract(_recentlyDisplayedSentences.Peek().DisplayTime).TotalHours > 24) + { + _recentlyDisplayedSentences.Dequeue(); + } + + // 获取当前已显示过的句子集合 + var recentTexts = _recentlyDisplayedSentences.Select(item => item.Text).ToHashSet(); + + // 筛选出未最近显示过的句子 + var availableSentences = _presetSentences.Where(s => !recentTexts.Contains(s)).ToList(); + + // 如果可用句子为空(全部句子都显示过了),则使用全部句子但优先选择最早显示过的 + if (availableSentences.Count == 0) + { + _logger.LogInformation("所有预设句子都已最近显示过,将选择最早显示的句子"); + // 从队列中获取并移除最早显示的句子 + var oldestSentence = _recentlyDisplayedSentences.Dequeue().Text; + + AddRecognizedTextToDisplay(oldestSentence, false); + + // 将这个句子重新添加到队列末尾,记录当前时间 + _recentlyDisplayedSentences.Enqueue((oldestSentence, DateTime.Now)); + } + else + { + // 从未最近显示过的句子中随机选择一条 + string randomText = availableSentences[new Random().Next(availableSentences.Count)]; + AddRecognizedTextToDisplay(randomText, false); + + // 添加到已显示队列 + _recentlyDisplayedSentences.Enqueue((randomText, DateTime.Now)); + + // 如果队列超出限制,移除最早的记录 + while (_recentlyDisplayedSentences.Count > _sentenceHistoryLimit) + { + _recentlyDisplayedSentences.Dequeue(); + } + } + + _logger.LogInformation($"当前已显示过的句子数量: {_recentlyDisplayedSentences.Count}/{_sentenceHistoryLimit}"); + } + catch (Exception ex) + { + _logger.LogError($"添加预设文本失败: {ex.Message}"); + // 使用一个默认句子防止程序崩溃 + AddRecognizedTextToDisplay("时光匆匆流逝,思念却越来越深。", false); + } } /// - /// 大屏客户端请求开始接收显示文本 + /// 从文件加载预设句子 /// - /// 处理任务 - public async Task StartReceivingDisplayText() + 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}"); + } + } + + /// + /// 从文件加载监控文本队列 + /// + 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>(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}"); + } + } + + /// + /// 保存监控文本队列到文件 + /// + 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}"); + } + } + + /// + /// 获取当前音频传输设置 + /// + /// 是否启用音频传输 + public async Task GetAudioStreamingSetting() { if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo)) { - _logger.LogWarning($"未注册的客户端尝试接收显示文本: {Context.ConnectionId}"); + _logger.LogWarning($"未注册的客户端尝试获取音频传输设置: {Context.ConnectionId}"); + throw new HubException("请先注册客户端"); + } + + _logger.LogInformation($"客户端获取当前音频传输设置: {Context.ConnectionId}, 类型: {clientInfo.ClientType}"); + return _configurationService.CurrentConfig.Network.EnableAudioStreaming; + } + + /// + /// 更新音频传输设置 + /// + /// 是否启用音频传输 + /// 处理任务 + 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.Display) + if (clientInfo.ClientType != ClientType.Monitor && clientInfo.ClientType != ClientType.WebAdmin) { - _logger.LogWarning($"非显示端客户端尝试接收显示文本: {Context.ConnectionId}"); - await Clients.Caller.SendAsync("Error", "只有显示端可以接收显示文本"); + _logger.LogWarning($"非监控或管理端客户端尝试更新音频传输设置: {Context.ConnectionId}, 类型: {clientInfo.ClientType}"); + await Clients.Caller.SendAsync("Error", "只有监控或管理端客户端可以更新音频传输设置"); return; } - _logger.LogInformation($"显示端开始接收文本: {Context.ConnectionId}"); + _logger.LogInformation($"更新音频传输设置: {Context.ConnectionId}, 新设置: {(enabled ? "开启" : "关闭")}"); - // 确保定时器已初始化 - InitializeDisplayTextTimer(); + // 更新配置 + _configurationService.CurrentConfig.Network.EnableAudioStreaming = enabled; - // 如果队列为空,添加一条初始文本 - if (_displayTextQueue.IsEmpty) + // 通知其他客户端音频传输设置已更改 + await Clients.Groups(new[] { "monitor", "webadmin" }).SendAsync("AudioStreamingChanged", enabled); + + // 保存配置到文件 + try { - AddFakeTextToQueue(); + 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, "更新音频传输设置成功,但保存配置失败"); + } + } + + /// + /// 释放资源 + /// + /// 是否正在处理Dispose + 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; + } + } + + /// + /// 释放资源 + /// + public new void Dispose() + { + Dispose(true); + base.Dispose(); + GC.SuppressFinalize(this); + } + + /// + /// 配置变更事件处理 + /// + /// 发送者 + /// 配置 + 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}"); } } @@ -906,27 +1168,6 @@ namespace ShengShengBuXi.Hubs } } - /// - /// 配置变更事件处理 - /// - /// 发送者 - /// 配置 - 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}"); - } - } - /// /// 开始监听音频 /// @@ -1086,180 +1327,5 @@ namespace ShengShengBuXi.Hubs _logger.LogInformation($"客户端获取当前显示模式: {Context.ConnectionId}, 类型: {clientInfo.ClientType}"); return _configurationService.CurrentConfig.DisplayType; } - - /// - /// 从文件加载监控文本队列 - /// - private void LoadMonitorTextQueueFromFile() - { - try - { - if (!File.Exists(_monitorTextQueueFilePath)) - { - _logger.LogInformation($"监控文本队列文件不存在: {_monitorTextQueueFilePath}"); - return; - } - - string json = File.ReadAllText(_monitorTextQueueFilePath); - if (string.IsNullOrEmpty(json)) - { - _logger.LogWarning("监控文本队列文件内容为空"); - return; - } - - var items = JsonConvert.DeserializeObject>(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}"); - } - } - - /// - /// 保存监控文本队列到文件 - /// - 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}"); - } - } - - /// - /// 获取当前音频传输设置 - /// - /// 是否启用音频传输 - public async Task 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; - } - - /// - /// 更新音频传输设置 - /// - /// 是否启用音频传输 - /// 处理任务 - 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, "更新音频传输设置成功,但保存配置失败"); - } - } - - /// - /// 释放资源 - /// - /// 是否正在处理Dispose - 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; - } - } - - /// - /// 释放资源 - /// - public new void Dispose() - { - Dispose(true); - base.Dispose(); - GC.SuppressFinalize(this); - } } } \ No newline at end of file diff --git a/ShengShengBuXi/Pages/Index.cshtml b/ShengShengBuXi/Pages/Index.cshtml index f4f7228..a5291ea 100644 --- a/ShengShengBuXi/Pages/Index.cshtml +++ b/ShengShengBuXi/Pages/Index.cshtml @@ -131,7 +131,7 @@ const CONFIG = { // 左侧容器配置 leftContainer: { - turnPageHeight: 0.7, // 左侧容器翻页的高度比例 + turnPageHeight: 0.55, // 左侧容器翻页的高度比例 fontSize: '16px', // 左侧列表文字大小 typewriterSpeed: 50 // 左侧文字打字机速度(毫秒) }, @@ -140,41 +140,24 @@ fontSize: '40px', // 右侧文字大小 fontWeight: '700', // 右侧文字粗细 fontStyle: 'italic', // 右侧文字样式 - typewriterSpeed: 50 // 右侧文字打字机速度(毫秒) + typewriterSpeed: 250 // 右侧文字打字机速度(毫秒),减慢到1/5 }, // 水波纹效果配置 waterEffect: { - enabled: true, // 是否开启水波纹 - minInterval: 800, // 最小触发间隔(毫秒) - maxInterval: 5000, // 最大触发间隔(毫秒) - simultaneousDrops: 3 // 同时触发的波纹数量 + enabled: true, // 是否开启水波纹 + minInterval: 1600, // 最小触发间隔(毫秒),增加时间 + maxInterval: 8000, // 最大触发间隔(毫秒),增加时间 + simultaneousDrops: 2, // 同时触发的波纹数量,减少到2个 + fadeOutSpeed: 2000, // 文字渐隐效果的速度(毫秒),原速度的2倍 + centerBias: 0.6, // 涟漪靠近中心区域的偏好值(0-1),0为随机,1为只在中心区域 + largeDrop: { + probability: 0.2, // 大涟漪出现的概率降低到0.2 + size: 80 // 大涟漪的大小 + } }, // 预设的中文句子数组 sentences: [ - "记得每到夏天傍晚,您就摇着蒲扇坐在藤椅里,把切好的西瓜最甜那块硬塞给我,自己却啃着靠近皮的白瓤,还笑着说:外公就爱这口,清爽。", - "女儿啊 花开了 你否到了吗?", - "外公,你在那边过的还好么大家都很想称,记得常回家看看", - "外公,今天窗台上的茉莉开了,白盈盈的,就像以前您总别在中山装口袋上的那朵。", - "外公,巷口那家老茶馆拆了,您最爱坐的靠窗位置再也找不到了,就像再也找不到您一样。", - "整理旧物时发现您用红绳缠好的象棋,每一处磨损都藏着您教我'马走日'时手心的温度。", - "今早闻到槐花香突然站住——您总说这味道像极了老家后山的夏天,现在我才懂得什么叫'睹物思人'。", - "菜场看见卖菱角的老伯,想起您总把最嫩的剥好放我碗里,自己却嚼着发苦的老根。", - "暴雨天膝盖又疼了吧?记得您总在这时候熬姜汤,说'老寒腿最懂天气预报'。", - "您养的那盆君子兰今年抽了七支花箭,比您走那年还多两枝,定是替您来看我的。", - "小满那天不自觉煮了两人份的腊肉饭,盛完才想起再没人把肥肉挑走给我留精瘦的。", - "儿童节路过小学,梧桐树下空荡荡的——再没有举着冰糖葫芦等我的驼背身影了。", - "冬至包饺子时手一抖,捏出您教的麦穗花边,滚水冒的蒸汽突然就迷了眼睛。", - "昨夜梦见您穿着洗白的蓝布衫,在晒谷场对我笑,醒来枕巾湿了半边。", - "蝉鸣最响的午后,恍惚听见竹椅吱呀声,转头却只看见墙上相框里的您。", - "您走后,再没人把枇杷核仔细包进手帕,笑着说'留着明年给囡囡种'。", - "整理遗物时数出38张火车票,全是往返我读书城市的,票根都磨出了毛边。", - "清明雨把墓碑冲洗得发亮,就像您当年总把搪瓷缸擦得能照见人影。", - "今天教女儿念'慈母手中线',她突然问:'太外公的诗集能读给我听吗?'", - "翻到您留下的老照片,那件洗得发白的中山装上还别着茉莉花,仿佛还能闻到淡淡的清香。", - "傍晚的蝉鸣声里,总错觉能听到您哼着那首走了调的小曲,在巷子口唤我回家吃饭。", - "您种的葡萄藤今年结了好多串,可再没有人像您那样,把最紫的摘下来悄悄塞进我口袋。", - "下雨天膝盖隐隐作痛时,总会想起您泡的姜茶,热气氤氲中您笑着说'老了才知道疼'。", - "整理书房时,发现您用毛笔在旧日历背面写的家训,墨迹晕染处都是您颤抖的手印。" + ] }; @@ -201,7 +184,7 @@ * @@param {Function} callback - 渐隐完成后的回调函数 */ function fadeOutText(selector, callback) { - $(selector).fadeOut(1000, function () { + $(selector).fadeOut(CONFIG.waterEffect.fadeOutSpeed, function () { $(this).html(''); $(this).show(); if (callback) callback(); @@ -454,7 +437,7 @@ } else { $('.water-effect').ripples({ resolution: 1024, - dropRadius: 3, + dropRadius: 1.5, perturbance: 0 }); @@ -464,9 +447,41 @@ */ function createRippleTimer() { return function randomRipple() { - let x1 = Math.random() * $(".water-effect").width(); - let y1 = Math.random() * $(".water-effect").height(); - triggerRipple(".water-effect", x1, y1); + let width = $(".water-effect").width(); + let height = $(".water-effect").height(); + + // 中心坐标 + let centerX = width / 2; + let centerY = height / 2; + + // 根据中心偏好生成随机位置 + let randomX, randomY; + + if (Math.random() > CONFIG.waterEffect.centerBias) { + // 随机位置 + randomX = Math.random() * width; + randomY = Math.random() * height; + + // 避免位于上1/3区域 + if (randomY < height / 3) { + randomY += height / 3; + } + } else { + // 靠近中心位置 + let radius = Math.min(width, height) * 0.3; // 中心区域半径 + let angle = Math.random() * Math.PI * 2; // 随机角度 + let distance = Math.random() * radius; // 随机距离,但不超过radius + + randomX = centerX + Math.cos(angle) * distance; + randomY = centerY + Math.sin(angle) * distance; + } + + // 决定是否创建大涟漪 + let dropSize = Math.random() < CONFIG.waterEffect.largeDrop.probability ? + CONFIG.waterEffect.largeDrop.size : 50; + + triggerRipple(".water-effect", randomX, randomY, dropSize); + let nextTime = Math.random() * (CONFIG.waterEffect.maxInterval - CONFIG.waterEffect.minInterval) + CONFIG.waterEffect.minInterval; setTimeout(randomRipple, nextTime); }; @@ -490,10 +505,34 @@ * @@param {string} selector - 目标元素选择器 * @@param {number} x - X坐标 * @@param {number} y - Y坐标 + * @@param {number} size - 涟漪大小 */ - function triggerRipple(selector, x, y) { - $(selector).ripples("drop", x, y, 50, 0.3); + function triggerRipple(selector, x, y, size = 50) { + $(selector).ripples("drop", x, y, size, 0.15); } + + // 页面可见性变化检测,用于处理页面切换到后台时暂停涟漪效果 + document.addEventListener('visibilitychange', function() { + if (document.hidden) { + // 页面不可见时,停止涟漪生成 + console.log("页面切换到后台,停止涟漪生成"); + // 可以添加额外逻辑来暂停或减少涟漪效果 + } else { + // 页面可见时,可以恢复涟漪生成 + console.log("页面回到前台,继续涟漪生成"); + // 可以添加额外逻辑来恢复涟漪效果 + + // 如果需要,可以重置涟漪效果 + if (CONFIG.waterEffect.enabled) { + $('.water-effect').ripples("destroy"); + $('.water-effect').ripples({ + resolution: 1024, + dropRadius: 1.5, + perturbance: 0 + }); + } + } + }); diff --git a/ShengShengBuXi/Pages/Monitor.cshtml b/ShengShengBuXi/Pages/Monitor.cshtml index f241485..8f555a6 100644 --- a/ShengShengBuXi/Pages/Monitor.cshtml +++ b/ShengShengBuXi/Pages/Monitor.cshtml @@ -21,8 +21,8 @@ 未检测到通话 - -
+ + - +
- + - - + + + + +
+ +
@@ -98,7 +103,7 @@ - +
@@ -107,7 +112,7 @@ 添加到显示
@@ -173,11 +178,13 @@ // 当前播放的音频元素和按钮引用 let currentAudio = null; let currentPlayButton = null; - + // 实时音频相关变量 let audioContext = null; let audioStreamSource = null; let isAudioStreamEnabled = false; + let audioGainNode = null; + let currentVolume = 1.0; // 默认音量为1.0 (100%) // 调试日志 function log(message) { @@ -206,7 +213,7 @@ } // 显示消息提示 - function showMessage(message, type = 'info') { + function showMessage(message, type = 'info', duration = 5000) { const messageArea = document.getElementById("message-area"); const alert = document.createElement("div"); alert.className = `alert alert-${type} alert-dismissible fade show`; @@ -216,11 +223,11 @@ `; messageArea.appendChild(alert); - // 5秒后自动移除 + // 自动移除 setTimeout(() => { alert.classList.remove('show'); setTimeout(() => alert.remove(), 150); - }, 5000); + }, duration); } // 更新连接状态 @@ -304,7 +311,7 @@ // 获取当前显示模式 getServerDisplayType(); - + // 获取当前音频流设置 getServerAudioStreamingSetting(); @@ -361,7 +368,7 @@ showMessage(message, "danger"); } }); - + // 接收实时音频数据 connection.on("ReceiveAudioData", (audioData) => { if (isAudioStreamEnabled && callInProgress) { @@ -371,13 +378,13 @@ playRealTimeAudio(audioData); } }); - + // 音频流设置更新消息 connection.on("AudioStreamingChanged", (enabled) => { log(`服务器音频流设置已更改为: ${enabled ? "开启" : "关闭"}`); updateAudioStreamingUI(enabled); }); - + // 音频流设置更新结果 connection.on("AudioStreamingUpdated", (success, message) => { if (success) { @@ -408,7 +415,7 @@ indicator.style.backgroundColor = "red"; statusText.textContent = "未检测到通话"; } - + // 有新通话时刷新监控列表 loadMonitorTextList(); } @@ -483,22 +490,22 @@ const shortText = text.length > 10 ? text.substring(0, 10) : text; listItem.innerHTML = ` -
- 【${shortText}】 -
- - - +
+ 【${shortText}】 +
+ + + +
-
-
${formattedDate}
- `; +
${formattedDate}
+ `; container.appendChild(listItem); }); @@ -510,7 +517,7 @@ log("无法删除监控文本:未连接或ID无效"); return; } - + log(`正在删除监控文本: ${id}`); connection.invoke("DelMonitorText", id) @@ -551,12 +558,12 @@ // 如果当前是暂停状态,则继续播放 log(`继续播放音频: ${path}`); currentAudio.play() - .then(() => { + .then(() => { // 更改按钮图标为暂停 buttonIcon.className = "bi bi-pause-fill"; showMessage("继续播放音频", "info"); - }) - .catch(err => { + }) + .catch(err => { showMessage(`播放失败: ${err}`, "danger"); log(`继续播放失败: ${err}`); }); @@ -584,6 +591,9 @@ // 创建新的音频元素 const audioElement = new Audio(path); + + // 应用当前音量设置 + audioElement.volume = currentVolume; // 设置当前播放的音频和按钮 currentAudio = audioElement; @@ -600,7 +610,7 @@ }); audioElement.addEventListener('loadeddata', () => { - log(`音频已加载,开始播放: ${path}`); + log(`音频已加载,开始播放: ${path}, 音量: ${currentVolume * 100}%`); }); audioElement.addEventListener('ended', () => { @@ -701,11 +711,11 @@ const shortText = text.length > 5 ? text.substring(0, 5) : text; listItem.innerHTML = ` -
- ${formattedDate} -
-
【${shortText}】
- `; +
+ ${formattedDate} +
+
【${shortText}】
+ `; container.appendChild(listItem); }); @@ -739,17 +749,17 @@ } }); }); - + // 监听音频传输单选按钮的变化 const audioStreamingButtons = document.querySelectorAll('input[name="audioStreaming"]'); audioStreamingButtons.forEach(radio => { radio.addEventListener('change', function () { const enabled = parseInt(this.value) === 1; log(`音频传输已切换为: ${enabled ? "开启" : "关闭"}`); - + // 设置本地音频流状态 isAudioStreamEnabled = enabled; - + // 如果开启了音频流,初始化音频上下文 if (enabled && callInProgress) { initAudioContext(); @@ -793,9 +803,24 @@ // 设置显示模式和音频流监听器 setupDisplayModeListeners(); + + // 设置音量控制监听器 + setupVolumeControl(); // 初始化工具提示 setTimeout(initTooltips, 1000); + + // 默认设置为手动显示模式 + document.getElementById("displayMode1").checked = true; + + // 默认开启音频传输 + document.getElementById("audioStreaming1").checked = true; + isAudioStreamEnabled = true; + + // 显示当前数据处理模式的提示消息 + setTimeout(() => { + showMessage("当前为手动处理数据模式,需要您手动审核并添加文本到显示队列", "info", 10000); + }, 1500); }); // 页面卸载前清理资源 @@ -803,7 +828,7 @@ if (refreshDisplayInterval) { clearInterval(refreshDisplayInterval); } - + // 关闭音频上下文 if (audioContext) { audioContext.close().catch(e => console.log("关闭音频上下文失败: " + e)); @@ -1026,15 +1051,15 @@ loadDisplayTextList(); showMessage("文本已添加到显示队列并从监控列表移除", "success"); - } else { + } else { log("监控文本删除失败"); showMessage("文本已添加到显示队列,但从监控列表移除失败", "warning"); // 刷新右侧显示文本列表 loadDisplayTextList(); } - }) - .catch(err => { + }) + .catch(err => { log(`删除监控文本失败: ${err}`); showMessage("文本已添加到显示队列,但从监控列表移除失败", "warning"); @@ -1067,55 +1092,58 @@ log("获取音频流设置失败: " + err); }); } - + // 更新音频流设置UI function updateAudioStreamingUI(enabled) { // 更新单选按钮状态 document.getElementById(`audioStreaming${enabled ? 1 : 0}`).checked = true; - + // 更新本地状态 isAudioStreamEnabled = enabled; - + log(`UI音频流设置已更新为: ${enabled ? "开启" : "关闭"}`); } - + + // 设置音量控制监听器 + function setupVolumeControl() { + const volumeControl = document.getElementById('volumeControl'); + if (volumeControl) { + volumeControl.addEventListener('input', function(e) { + currentVolume = parseFloat(e.target.value); + log(`音量已调整为: ${currentVolume * 100}%`); + + // 如果已经创建了增益节点,立即应用音量设置 + if (audioGainNode) { + audioGainNode.gain.value = currentVolume; + } + + // 同时调整已播放的录音音量 + if (currentAudio) { + currentAudio.volume = currentVolume; + } + }); + } + } + // 初始化音频上下文 function initAudioContext() { + if (audioContext) return; // 避免重复初始化 + try { - if (audioContext) { - // 如果已存在音频上下文,检查其状态 - if (audioContext.state === 'suspended') { - audioContext.resume().then(() => { - log("音频上下文已恢复"); - }).catch(e => { - log("恢复音频上下文失败: " + e); - }); - return; - } else if (audioContext.state === 'running') { - // 已经在运行,无需重新创建 - log("音频上下文已在运行中"); - return; - } else { - // 如果状态是closed或其他异常状态,尝试关闭并重新创建 - try { - audioContext.close(); - } catch (e) { - log("关闭旧音频上下文失败: " + e); - } - } - } + // 创建音频上下文 + const AudioContext = window.AudioContext || window.webkitAudioContext; + audioContext = new AudioContext(); - // 创建音频上下文,使用较低延迟选项 - window.AudioContext = window.AudioContext || window.webkitAudioContext; - const options = { - latencyHint: 'interactive', - sampleRate: 16000 // 与服务器端配置匹配 - }; - audioContext = new AudioContext(options); + // 创建增益节点用于控制音量 + audioGainNode = audioContext.createGain(); + audioGainNode.gain.value = currentVolume; // 设置初始音量 + audioGainNode.connect(audioContext.destination); + + log("音频上下文已初始化,创建了增益节点,初始音量: " + currentVolume); - // 解决iOS/Safari音频上下文自动暂停问题 + // 如果音频上下文处于挂起状态,需要用户交互来激活 if (audioContext.state === 'suspended') { - const resumeAudio = function() { + const resumeAudio = function () { audioContext.resume().then(() => { log("用户交互已激活音频上下文"); document.removeEventListener('click', resumeAudio); @@ -1123,29 +1151,29 @@ document.removeEventListener('touchend', resumeAudio); }); }; - + document.addEventListener('click', resumeAudio); document.addEventListener('touchstart', resumeAudio); document.addEventListener('touchend', resumeAudio); } - + log("音频上下文已初始化,状态: " + audioContext.state + ", 采样率: " + audioContext.sampleRate); } catch (e) { log("无法创建音频上下文: " + e); showMessage("无法初始化音频播放: " + e, "danger"); } } - + // 播放实时音频 function playRealTimeAudio(audioData) { if (!audioContext || !isAudioStreamEnabled) return; - + try { log("接收到实时音频数据..."); - + // 检查音频数据类型 log(`音频数据类型: ${Object.prototype.toString.call(audioData)}, 长度: ${audioData ? (audioData.length || audioData.byteLength || 'unknown') : 'null'}`); - + // 处理接收到的音频数据 let pcmData; if (audioData instanceof Uint8Array) { @@ -1166,7 +1194,7 @@ } else if (audioData.buffer && audioData.buffer instanceof ArrayBuffer) { pcmData = new Uint8Array(audioData.buffer); } else { - // 尝试将对象转换为JSON并记录以便调试 + // 尝试将对象转换为JSON并记录以便调试 try { log("对象数据:" + JSON.stringify(audioData).substring(0, 100)); } catch (e) { @@ -1188,7 +1216,7 @@ pcmData[i] = binary.charCodeAt(i); } log("已从Base64字符串转换为Uint8Array"); - } catch (e) { + } catch (e) { log("Base64转换失败: " + e); return; } @@ -1196,22 +1224,22 @@ return; } } - + // 检查数据是否为空或过小 if (!pcmData || pcmData.length < 2) { log("音频数据无效或过小"); return; } - + log(`处理音频数据: 长度=${pcmData.length} 字节`); - + // 确保数据长度是偶数(16位样本需要2个字节) const validLength = Math.floor(pcmData.length / 2) * 2; if (validLength < pcmData.length) { log(`调整音频数据长度从 ${pcmData.length} 到 ${validLength} 字节`); pcmData = pcmData.slice(0, validLength); } - + // 获取有效的DataView let dataView; try { @@ -1230,10 +1258,10 @@ return; } } - + // 将PCM数据(16位整数)转换为32位浮点数组 const floatData = new Float32Array(pcmData.length / 2); - + // 转换16位PCM到32位浮点(使用try-catch保护读取操作) try { for (let i = 0; i < floatData.length; i++) { @@ -1252,11 +1280,11 @@ log("转换音频数据失败: " + e); return; } - + // 创建一个包含音频数据的AudioBuffer const sampleRate = 16000; // 采样率固定为16kHz const buffer = audioContext.createBuffer(1, floatData.length, sampleRate); - + // 将浮点数据复制到AudioBuffer的第一个通道 try { const channel = buffer.getChannelData(0); @@ -1265,15 +1293,22 @@ log("设置音频通道数据失败: " + e); return; } - + // 创建音频源并连接到音频输出 const source = audioContext.createBufferSource(); source.buffer = buffer; - source.connect(audioContext.destination); + // 连接到增益节点而不是直接连接到输出 + source.connect(audioGainNode); + + // 确保音量设置被应用 + if (audioGainNode) { + audioGainNode.gain.value = currentVolume; + } + // 播放 source.start(0); - log("实时音频播放中..."); + log(`实时音频播放中...音量: ${currentVolume * 100}%`); } catch (e) { log("处理实时音频失败: " + e); } diff --git a/ShengShengBuXi/ShengShengBuXi.csproj b/ShengShengBuXi/ShengShengBuXi.csproj index 36a4256..ff7035d 100644 --- a/ShengShengBuXi/ShengShengBuXi.csproj +++ b/ShengShengBuXi/ShengShengBuXi.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -30,4 +30,14 @@ + + + + + + + PreserveNewest + + + diff --git a/ShengShengBuXi/config.json b/ShengShengBuXi/config.json index 5b8f53a..46cfd7c 100644 --- a/ShengShengBuXi/config.json +++ b/ShengShengBuXi/config.json @@ -18,8 +18,8 @@ "recordingDeviceNumber": 0, "sampleRate": 16000, "channels": 1, - "bufferMilliseconds": 100, - "silenceThreshold": 0.02, + "bufferMilliseconds": 50, + "silenceThreshold": 0.1, "silenceTimeoutSeconds": 30, "allowUserHangup": true, "uploadRecordingToServer": false, @@ -31,19 +31,19 @@ "callFlow": { "waitForPickupMinSeconds": 3, "waitForPickupMaxSeconds": 6, - "playPickupProbability": 0.15 + "playPickupProbability": 0.5 }, "network": { - "serverUrl": "http://localhost:5140/audiohub", - "reconnectAttempts": 3, + "serverUrl": "http://115.159.44.16/audiohub", + "reconnectAttempts": 30, "reconnectDelayMs": 2000, "enableSpeechToText": true, "enableAudioStreaming": true, - "heartbeatIntervalSeconds": 30, + "heartbeatIntervalSeconds": 15, "tencentCloudASR": { - "appId": "1320384962", - "secretId": "AKIDX0LVSJBqIWWJsEMOQH6qQn7FAsPtAFB7", - "secretKey": "rdMuSpzvd8PTKyWq45uX1JNDrIWWIuIT", + "appId": "", + "secretId": "", + "secretKey": "", "engineModelType": "16k_zh", "voiceFormat": "1", "filterDirty": true, @@ -52,7 +52,7 @@ "needVad": true } }, - "displayType": 1, + "displayType": 0, "minDisplayText": 3, "maxDisplayText": 60 } \ No newline at end of file diff --git a/ShengShengBuXi/config/sentences.txt b/ShengShengBuXi/config/sentences.txt new file mode 100644 index 0000000..de66a9a Binary files /dev/null and b/ShengShengBuXi/config/sentences.txt differ