提交
This commit is contained in:
parent
f761157a1c
commit
067b3d11bc
|
|
@ -73,35 +73,23 @@ namespace ShengShengBuXi.Hubs
|
|||
/// <summary>
|
||||
/// 监控文本队列的持久化文件路径
|
||||
/// </summary>
|
||||
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");
|
||||
/// <summary>
|
||||
/// 预设句子文件路径
|
||||
/// </summary>
|
||||
private static readonly string _sentencesFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config/sentences.txt");
|
||||
/// <summary>
|
||||
/// 预设句子列表
|
||||
/// </summary>
|
||||
private static List<string> _presetSentences = new List<string>();
|
||||
/// <summary>
|
||||
/// 已显示过的预设句子队列及显示时间,用于防止短时间内重复显示
|
||||
/// </summary>
|
||||
private static readonly Queue<(string Text, DateTime DisplayTime)> _recentlyDisplayedSentences = new Queue<(string, DateTime)>();
|
||||
/// <summary>
|
||||
/// 防止重复显示的句子数量
|
||||
/// </summary>
|
||||
private static readonly int _sentenceHistoryLimit = 20;
|
||||
|
||||
/// <summary>
|
||||
/// 初始化音频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<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;
|
||||
}
|
||||
|
||||
|
|
@ -552,14 +543,14 @@ namespace ShengShengBuXi.Hubs
|
|||
/// <returns>显示文本列表</returns>
|
||||
public async Task<List<DisplayText>> 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
|
|||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 大屏客户端请求开始接收显示文本
|
||||
/// 从文件加载预设句子
|
||||
/// </summary>
|
||||
/// <returns>处理任务</returns>
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <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}");
|
||||
_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.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, "更新音频传输设置成功,但保存配置失败");
|
||||
}
|
||||
}
|
||||
|
||||
/// <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}");
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -906,27 +1168,6 @@ namespace ShengShengBuXi.Hubs
|
|||
}
|
||||
}
|
||||
|
||||
/// <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>
|
||||
|
|
@ -1086,180 +1327,5 @@ namespace ShengShengBuXi.Hubs
|
|||
_logger.LogInformation($"客户端获取当前显示模式: {Context.ConnectionId}, 类型: {clientInfo.ClientType}");
|
||||
return _configurationService.CurrentConfig.DisplayType;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从文件加载监控文本队列
|
||||
/// </summary>
|
||||
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<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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@
|
|||
<span id="status-text" class="me-3">未检测到通话</span>
|
||||
</div>
|
||||
|
||||
<!-- 单选按钮组 -->
|
||||
<div class="btn-group" role="group">
|
||||
<!-- 单选按钮组 - 隐藏自动识别显示选项 -->
|
||||
<div class="btn-group" role="group" style="display: none;">
|
||||
<input type="radio" class="btn-check" name="displayMode" id="displayMode0" value="0"
|
||||
checked>
|
||||
<label class="btn btn-outline-primary" for="displayMode0">自动识别显示</label>
|
||||
|
|
@ -31,13 +31,18 @@
|
|||
<label class="btn btn-outline-primary" for="displayMode1">手动处理显示</label>
|
||||
</div>
|
||||
|
||||
<!-- 音频传输开关 -->
|
||||
<!-- 音频传输开关 - 隐藏关闭音频传输选项 -->
|
||||
<div class="btn-group ms-3" role="group">
|
||||
<input type="radio" class="btn-check" name="audioStreaming" id="audioStreaming1" value="1">
|
||||
<input type="radio" class="btn-check" name="audioStreaming" id="audioStreaming1" value="1" checked>
|
||||
<label class="btn btn-outline-success" for="audioStreaming1">开启音频传输</label>
|
||||
|
||||
<input type="radio" class="btn-check" name="audioStreaming" id="audioStreaming0" value="0" checked>
|
||||
<label class="btn btn-outline-danger" for="audioStreaming0">关闭音频传输</label>
|
||||
<input type="radio" class="btn-check" name="audioStreaming" id="audioStreaming0" value="0" style="display: none;">
|
||||
<label class="btn btn-outline-danger" for="audioStreaming0" style="display: none;">关闭音频传输</label>
|
||||
|
||||
<!-- 音量控制滑块 -->
|
||||
<div class="ms-3 d-flex align-items-center">
|
||||
<label for="volumeControl" class="me-2"><i class="bi bi-volume-up"></i></label>
|
||||
<input type="range" class="form-range" min="0" max="1" step="0.1" value="1.0" id="volumeControl" style="width: 100px;">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -98,7 +103,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 按钮组区域 -->
|
||||
<!-- 按钮组区域 - 隐藏添加并移除按钮 -->
|
||||
<div id="action-buttons-area" class="mt-3" style="height: 20%;">
|
||||
<div class="card h-100">
|
||||
<div class="card-body d-flex justify-content-center align-items-center">
|
||||
|
|
@ -107,7 +112,7 @@
|
|||
<i class="bi bi-plus-circle"></i> 添加到显示
|
||||
</button>
|
||||
<button id="add-and-remove-btn" class="btn btn-warning" data-bs-toggle="tooltip"
|
||||
title="将文本添加到显示队列,并从监控列表中移除当前选中项" onclick="addDisplayTextAndRemoveMonitor()">
|
||||
title="将文本添加到显示队列,并从监控列表中移除当前选中项" onclick="addDisplayTextAndRemoveMonitor()" style="display: none;">
|
||||
<i class="bi bi-arrow-right-circle"></i> 添加并移除
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -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 = `
|
||||
<div class="d-flex justify-content-between align-items-start mb-1">
|
||||
<small class="text-muted">【${shortText}】</small>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-primary btn-sm" onclick="playAudio('${item.recordingPath || ''}', this)">
|
||||
<i class="bi bi-play-fill"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="downloadRecording('${item.recordingPath || ''}')">
|
||||
<i class="bi bi-download"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-danger btn-sm" onclick="deleteMonitorText('${item.id}')">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
<div class="d-flex justify-content-between align-items-start mb-1">
|
||||
<small class="text-muted">【${shortText}】</small>
|
||||
<div class="btn-group btn-group-sm">
|
||||
<button class="btn btn-outline-primary btn-sm" onclick="playAudio('${item.recordingPath || ''}', this)">
|
||||
<i class="bi bi-play-fill"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-secondary btn-sm" onclick="downloadRecording('${item.recordingPath || ''}')">
|
||||
<i class="bi bi-download"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-danger btn-sm" onclick="deleteMonitorText('${item.id}')">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>${formattedDate}</div>
|
||||
`;
|
||||
<div>${formattedDate}</div>
|
||||
`;
|
||||
|
||||
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 = `
|
||||
<div class="mb-1">
|
||||
<small class="text-muted">${formattedDate}</small>
|
||||
</div>
|
||||
<div>【${shortText}】</div>
|
||||
`;
|
||||
<div class="mb-1">
|
||||
<small class="text-muted">${formattedDate}</small>
|
||||
</div>
|
||||
<div>【${shortText}】</div>
|
||||
`;
|
||||
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
|
|
@ -30,4 +30,14 @@
|
|||
<PackageReference Include="System.Net.WebSockets.Client" Version="4.3.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="config\" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="config\sentences.txt">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
BIN
ShengShengBuXi/config/sentences.txt
Normal file
BIN
ShengShengBuXi/config/sentences.txt
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user