This commit is contained in:
zpc 2025-03-28 01:38:44 +08:00
parent f761157a1c
commit 067b3d11bc
6 changed files with 552 additions and 402 deletions

View File

@ -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);
}
}
}

View File

@ -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>

View File

@ -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);
}

View File

@ -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>

View File

@ -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
}

Binary file not shown.