This commit is contained in:
zpc 2025-03-30 00:51:07 +08:00
parent c626311dbf
commit 9f9381de9a
19 changed files with 7198 additions and 1475 deletions

View File

@ -11,74 +11,6 @@ namespace ShengShengBuXi.ConsoleApp;
public class Program
{
private static readonly string AudioPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "mp3");
private static readonly ConcurrentDictionary<int, (WaveOutEvent Device, AudioFileReader Reader)> KeyToneDevices = new();
private static WaveOutEvent? waitingToneDevice;
private static AudioFileReader? waitingToneReader;
private static volatile bool isRecording = false;
private static DateTime lastKeyPressTime = DateTime.MinValue;
private static List<int> pressedKeys = new();
private static volatile bool isWaitingTonePlaying = false;
private static CancellationTokenSource? waitingToneCts;
private static readonly CancellationTokenSource programCts = new();
private static readonly HashSet<int> currentPressedKeys = new();
private static readonly Timer resetTimer;
private static readonly Timer dialOutTimer;
// Windows API
private const int WH_KEYBOARD_LL = 13;
private const int WM_KEYDOWN = 0x0100;
private const int WM_KEYUP = 0x0101;
private static IntPtr hookHandle = IntPtr.Zero;
private static HookProc? hookProc;
// 数字键盘的虚拟键码 (VK_NUMPAD0 - VK_NUMPAD9)
private static readonly int[] numpadKeys = Enumerable.Range(0x60, 10).ToArray();
// 回车键的虚拟键码
private const int VK_RETURN = 0x0D;
// 新增录音相关变量
private static WaveInEvent? waveIn = null;
private static WaveFileWriter? waveWriter = null;
private static string recordingFilePath = "";
private static DateTime lastSoundTime = DateTime.MinValue;
private static Timer? silenceTimer = null;
private static readonly float silenceThreshold = 0.02f; // 静音阈值
private static volatile bool isHangUpKeyPressed = false;
static Program()
{
resetTimer = new Timer(CheckResetTimeout, null, Timeout.Infinite, Timeout.Infinite);
dialOutTimer = new Timer(CheckDialOutTimeout, null, Timeout.Infinite, Timeout.Infinite);
}
[StructLayout(LayoutKind.Sequential)]
private struct KBDLLHOOKSTRUCT
{
public uint vkCode;
public uint scanCode;
public uint flags;
public uint time;
public IntPtr dwExtraInfo;
}
private delegate IntPtr HookProc(int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")]
private static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll")]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll")]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll")]
private static extern IntPtr GetModuleHandle(string? lpModuleName);
[DllImport("user32.dll")]
private static extern short GetAsyncKeyState(int vKey);
public static async Task Main(string[] args)
{
@ -193,813 +125,4 @@ public class Program
return services.BuildServiceProvider();
}
private static void InitializeAudioDevice()
{
try
{
waitingToneDevice = new WaveOutEvent();
waitingToneReader = new AudioFileReader(Path.Combine(AudioPath, "等待嘟音.wav"));
waitingToneDevice.Init(waitingToneReader);
}
catch (Exception ex)
{
Console.WriteLine($"初始化音频设备失败: {ex.Message}");
throw;
}
}
private static async Task StartKeyboardListener()
{
while (!programCts.Token.IsCancellationRequested)
{
try
{
// 检查回车键
bool isEnterPressed = (GetAsyncKeyState(VK_RETURN) & 0x8000) != 0;
if (isEnterPressed)
{
// 添加一点延迟,防止连续检测到按键
await Task.Delay(100, programCts.Token);
// 再次检查回车键状态,确保不是误触或抖动
bool stillPressed = (GetAsyncKeyState(VK_RETURN) & 0x8000) != 0;
if (!stillPressed)
{
continue;
}
if (isRecording)
{
// 如果正在录音中,回车键被视为挂断信号
Console.WriteLine("检测到回车键,用户挂断...");
isHangUpKeyPressed = true;
// 等待一段时间,防止重复触发
await Task.Delay(500, programCts.Token);
continue;
}
// 如果等待音正在播放,停止它
if (isWaitingTonePlaying)
{
waitingToneCts?.Cancel();
// 确保等待音停止
while (isWaitingTonePlaying)
{
await Task.Delay(10);
}
}
Console.WriteLine("检测到回车键,开始拨出...");
// 非阻塞方式启动录音,不等待其完成
_ = StartRecording();
// 延迟防止重复触发
await Task.Delay(500, programCts.Token);
continue;
}
foreach (int key in numpadKeys)
{
int digit = key - 0x60; // 将虚拟键码转换为数字
bool isKeyDown = (GetAsyncKeyState(key) & 0x8000) != 0;
if (isKeyDown)
{
if (isRecording)
{
Console.WriteLine("正在录音中,无法点击按键...");
await Task.Delay(1000, programCts.Token);
continue;
}
if (!currentPressedKeys.Contains(digit))
{
Console.WriteLine($"按下数字键: {digit}");
currentPressedKeys.Add(digit);
await HandleDigitKeyPress(digit);
}
}
else
{
if (currentPressedKeys.Contains(digit))
{
Console.WriteLine($"释放数字键: {digit}");
currentPressedKeys.Remove(digit);
await HandleDigitKeyRelease(digit);
}
}
}
await Task.Delay(10, programCts.Token); // 降低CPU使用率
}
catch (OperationCanceledException)
{
break;
}
catch (Exception ex)
{
Console.WriteLine($"键盘监听出错: {ex.Message}");
}
}
}
private static async Task StartPlayingWaitingTone()
{
try
{
waitingToneCts?.Cancel();
waitingToneCts?.Dispose();
waitingToneCts = CancellationTokenSource.CreateLinkedTokenSource(programCts.Token);
var token = waitingToneCts.Token;
isWaitingTonePlaying = true;
while (!token.IsCancellationRequested)
{
try
{
if (waitingToneReader?.Position >= waitingToneReader?.Length)
{
waitingToneReader.Position = 0;
}
waitingToneDevice?.Play();
await Task.Delay(100, token);
}
catch (OperationCanceledException)
{
break;
}
}
}
catch (Exception ex)
{
Console.WriteLine($"播放等待音失败: {ex.Message}");
}
finally
{
isWaitingTonePlaying = false;
waitingToneDevice?.Stop();
}
}
private static async Task HandleDigitKeyPress(int digit)
{
try
{
// 记录按键
pressedKeys.Add(digit);
lastKeyPressTime = DateTime.Now;
Console.WriteLine($"按下数字键: {digit}, 当前已按键数: {pressedKeys.Count}");
// 如果是第一个按键,停止等待音
if (pressedKeys.Count == 1 && isWaitingTonePlaying)
{
waitingToneCts?.Cancel();
}
// 播放按键音
await PlayKeyTone(digit);
// 重置定时器
resetTimer.Change(10000, Timeout.Infinite); // 10秒后检查是否需要重置
// 检查是否需要拨出8位以上且5秒内无新按键
if (pressedKeys.Count >= 8)
{
// 重置拨出定时器5秒后检查是否需要拨出
dialOutTimer.Change(5000, Timeout.Infinite);
}
else
{
// 如果位数不足8位停止拨出定时器
dialOutTimer.Change(Timeout.Infinite, Timeout.Infinite);
}
}
catch (Exception ex)
{
Console.WriteLine($"处理按键 {digit} 失败: {ex.Message}");
}
}
private static void CheckResetTimeout(object? state)
{
try
{
// 如果正在录音中,不执行重置操作
if (isRecording)
{
return;
}
if (pressedKeys.Count > 0 && pressedKeys.Count < 8 &&
(DateTime.Now - lastKeyPressTime).TotalSeconds >= 10)
{
Console.WriteLine("10秒内未完成8位数字输入重置等待...");
pressedKeys.Clear();
KeyToneDevices.Clear();
_ = StartPlayingWaitingTone();
}
}
catch (Exception ex)
{
Console.WriteLine($"检查超时重置失败: {ex.Message}");
}
}
private static void CheckDialOutTimeout(object? state)
{
try
{
// 如果正在录音中,不执行拨出操作
if (isRecording)
{
return;
}
// 检查是否已经过了5秒且按键数大于等于8
if (pressedKeys.Count >= 8 && (DateTime.Now - lastKeyPressTime).TotalSeconds >= 5)
{
Console.WriteLine("5秒内无新按键开始拨出...");
// 非阻塞方式启动录音
_ = StartRecording();
}
}
catch (Exception ex)
{
Console.WriteLine($"检查拨出超时失败: {ex.Message}");
}
}
private static async Task HandleDigitKeyRelease(int digit)
{
try
{
if (KeyToneDevices.TryRemove(digit, out var deviceInfo))
{
deviceInfo.Device.Stop();
deviceInfo.Device.Dispose();
deviceInfo.Reader.Dispose();
}
}
catch (Exception ex)
{
Console.WriteLine($"处理按键 {digit} 释放失败: {ex.Message}");
}
}
private static async Task PlayKeyTone(int digit)
{
try
{
if (KeyToneDevices.ContainsKey(digit))
{
return; // 已经在播放中
}
var device = new WaveOutEvent();
var reader = new AudioFileReader(Path.Combine(AudioPath, $"{digit}.mp3"));
device.Init(reader);
if (KeyToneDevices.TryAdd(digit, (device, reader)))
{
device.Play();
}
else
{
device.Dispose();
reader.Dispose();
}
}
catch (Exception ex)
{
Console.WriteLine($"播放按键音 {digit} 失败: {ex.Message}");
}
}
private static async Task StartRecording()
{
// 创建一个CancellationTokenSource用于取消整个录音流程
using var recordingCts = CancellationTokenSource.CreateLinkedTokenSource(programCts.Token);
// 创建一个单独的任务来监视挂断信号
var hangupMonitorTask = Task.Run(async () =>
{
while (!recordingCts.Token.IsCancellationRequested)
{
// 检查是否收到挂断信号
if (isHangUpKeyPressed)
{
Console.WriteLine("录音流程中检测到挂断信号,即将终止流程...");
recordingCts.Cancel();
break;
}
await Task.Delay(50);
}
});
try
{
// 进入录音状态,停止所有定时器
isRecording = true;
isHangUpKeyPressed = false;
resetTimer.Change(Timeout.Infinite, Timeout.Infinite);
dialOutTimer.Change(Timeout.Infinite, Timeout.Infinite);
// 确保等待音已停止
if (isWaitingTonePlaying)
{
waitingToneCts?.Cancel();
// 等待等待音实际停止
while (isWaitingTonePlaying && !recordingCts.Token.IsCancellationRequested)
{
await Task.Delay(10);
}
}
// 如果已经收到取消信号,立即结束
if (recordingCts.Token.IsCancellationRequested)
{
throw new OperationCanceledException("录音流程被用户取消");
}
Console.WriteLine("开始录音流程...");
// 随机播放3-6秒的等待接电话音频
int waitTime = new Random().Next(3, 7); // 3到6秒
Console.WriteLine($"播放等待接电话音频,持续{waitTime}秒...");
try
{
await PlayAudioAndWait(Path.Combine(AudioPath, "等待接电话.mp3"), waitTime * 1000, true, recordingCts.Token);
}
catch (OperationCanceledException)
{
throw;
}
// 如果已经收到取消信号,立即结束
if (recordingCts.Token.IsCancellationRequested)
{
throw new OperationCanceledException("录音流程被用户取消");
}
// 15%概率播放电话接起音频
bool playPickup = new Random().NextDouble() < 0.15;
if (playPickup)
{
Console.WriteLine("播放电话接起音频...");
try
{
await PlayAudioAndWait(Path.Combine(AudioPath, "电话接起.mp3"), null, false, recordingCts.Token);
}
catch (OperationCanceledException)
{
throw;
}
if (recordingCts.Token.IsCancellationRequested)
{
throw new OperationCanceledException("录音流程被用户取消");
}
}
// 播放提示用户录音的音频
Console.WriteLine("播放提示用户录音音频...");
try
{
await PlayAudioAndWait(Path.Combine(AudioPath, "提示用户录音.mp3"), null, false, recordingCts.Token);
}
catch (OperationCanceledException)
{
throw;
}
if (recordingCts.Token.IsCancellationRequested)
{
throw new OperationCanceledException("录音流程被用户取消");
}
// 开始实际录音逻辑
Console.WriteLine("开始初始化录音设备...");
// 创建录音文件路径
string recordingsFolder = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "recordings");
if (!Directory.Exists(recordingsFolder))
{
Directory.CreateDirectory(recordingsFolder);
}
recordingFilePath = Path.Combine(recordingsFolder, $"recording_{DateTime.Now:yyyyMMdd_HHmmss}.wav");
// 初始化录音设备
await StartAudioRecording(recordingFilePath);
if (recordingCts.Token.IsCancellationRequested)
{
throw new OperationCanceledException("录音流程被用户取消");
}
// 创建一个等待完成的任务源
var recordingCompletionSource = new TaskCompletionSource<bool>();
// 启动静音检测计时器
lastSoundTime = DateTime.Now;
silenceTimer = new Timer(CheckSilence, recordingCompletionSource, 1000, 1000);
Console.WriteLine("播放滴提示音...");
try
{
await PlayAudioAndWait(Path.Combine(AudioPath, "滴提示音.wav"), null, false, recordingCts.Token);
}
catch (OperationCanceledException)
{
throw;
}
if (recordingCts.Token.IsCancellationRequested)
{
throw new OperationCanceledException("录音流程被用户取消");
}
Console.WriteLine("开始录制用户声音...");
// 创建一个任务等待录音结束(通过静音检测、挂断按键或取消)
var recordingTask = Task.Run(async () =>
{
try
{
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(recordingCts.Token);
// 注册取消处理程序
linkedCts.Token.Register(() =>
{
recordingCompletionSource.TrySetResult(true);
});
// 等待录音完成
await recordingCompletionSource.Task;
}
catch (Exception)
{
// 捕获所有异常,确保不会中断主流程
}
});
// 等待录音完成或取消
try
{
await Task.WhenAny(recordingTask, Task.Delay(Timeout.Infinite, recordingCts.Token));
}
catch (OperationCanceledException)
{
// 预期的取消异常,可以忽略
}
// 停止录音
StopAudioRecording();
Console.WriteLine("录音结束,重置状态...");
// 完全重置系统状态
CompletelyResetState();
Console.WriteLine("系统已重置,可以继续使用...");
}
catch (OperationCanceledException ex)
{
Console.WriteLine($"录音流程被取消: {ex.Message}");
// 确保录音设备被释放
StopAudioRecording();
// 取消时也完全重置系统状态
CompletelyResetState();
Console.WriteLine("系统已重置,可以继续使用...");
}
catch (Exception ex)
{
Console.WriteLine($"录音过程发生错误: {ex.Message}");
// 确保录音设备被释放
StopAudioRecording();
// 发生错误时也完全重置系统状态
CompletelyResetState();
}
}
private static void CheckSilence(object? state)
{
try
{
var completionSource = state as TaskCompletionSource<bool>;
// 如果用户按了回车键,立即结束录音
if (isHangUpKeyPressed)
{
Console.WriteLine("定时器检测到用户手动挂断");
completionSource?.TrySetResult(true);
return;
}
// 检查是否超过30秒没有声音
if ((DateTime.Now - lastSoundTime).TotalSeconds >= 30)
{
Console.WriteLine("检测到30秒无声音自动挂断");
completionSource?.TrySetResult(true);
}
}
catch (Exception ex)
{
Console.WriteLine($"静音检测错误: {ex.Message}");
}
}
private static async Task StartAudioRecording(string filePath)
{
try
{
// 创建录音设备
waveIn = new WaveInEvent
{
DeviceNumber = 0, // 使用默认录音设备
WaveFormat = new WaveFormat(16000, 1), // 16 kHz, 单声道
BufferMilliseconds = 100 //音频缓冲区的大小(以毫秒为单位)。这个参数控制着音频数据从麦克风读取并触发 DataAvailable 事件的频率。
};
// 创建文件写入器
waveWriter = new WaveFileWriter(filePath, waveIn.WaveFormat);
// 处理录音数据
waveIn.DataAvailable += (s, e) =>
{
try
{
// 将数据写入文件
waveWriter.Write(e.Buffer, 0, e.BytesRecorded);
// 检查是否有声音
if (HasSound(e.Buffer, e.BytesRecorded))
{
lastSoundTime = DateTime.Now;
}
}
catch (Exception ex)
{
Console.WriteLine($"录音数据处理错误: {ex.Message}");
}
};
// 录音完成事件
waveIn.RecordingStopped += (s, e) =>
{
// 在这里处理录音停止后的逻辑
Console.WriteLine("录音已停止");
};
// 开始录音
waveIn.StartRecording();
Console.WriteLine($"开始录音,保存到文件: {filePath}");
}
catch (Exception ex)
{
Console.WriteLine($"启动录音失败: {ex.Message}");
throw;
}
}
private static void StopAudioRecording()
{
try
{
// 停止静音检测计时器
silenceTimer?.Change(Timeout.Infinite, Timeout.Infinite);
silenceTimer?.Dispose();
silenceTimer = null;
// 停止录音
if (waveIn != null)
{
waveIn.StopRecording();
waveIn.Dispose();
waveIn = null;
}
// 关闭文件
if (waveWriter != null)
{
waveWriter.Dispose();
waveWriter = null;
Console.WriteLine($"录音已保存到: {recordingFilePath}");
}
}
catch (Exception ex)
{
Console.WriteLine($"停止录音失败: {ex.Message}");
}
}
private static bool HasSound(byte[] buffer, int bytesRecorded)
{
// 将字节数组转换为浮点数数组以计算音量
float maxVolume = 0;
// 对于16位PCM数据
for (int i = 0; i < bytesRecorded; i += 2)
{
if (i + 1 < bytesRecorded)
{
// 将两个字节转换为short16位
short sample = (short)((buffer[i + 1] << 8) | buffer[i]);
// 转换为-1.0到1.0范围内的浮点数
float normSample = sample / 32768.0f;
// 取绝对值并更新最大音量
float absSample = Math.Abs(normSample);
if (absSample > maxVolume)
{
maxVolume = absSample;
}
}
}
// 如果最大音量超过阈值,则认为有声音
return maxVolume > silenceThreshold;
}
private static async void CompletelyResetState()
{
try
{
// 确保录音设备被释放
StopAudioRecording();
// 清除录音状态
isRecording = false;
isHangUpKeyPressed = false;
// 清除按键记录
pressedKeys.Clear();
currentPressedKeys.Clear();
// 停止所有按键音
foreach (var (digit, deviceInfo) in KeyToneDevices.ToArray())
{
try
{
KeyToneDevices.TryRemove(digit, out _);
deviceInfo.Device.Stop();
deviceInfo.Device.Dispose();
deviceInfo.Reader.Dispose();
}
catch (Exception ex)
{
Console.WriteLine($"清理按键音设备失败: {ex.Message}");
}
}
// 重新开始播放等待音
_ = StartPlayingWaitingTone();
}
catch (Exception ex)
{
Console.WriteLine($"重置状态失败: {ex.Message}");
// 最后的保障措施
isRecording = false;
isHangUpKeyPressed = false;
pressedKeys.Clear();
currentPressedKeys.Clear();
KeyToneDevices.Clear();
// 尝试重新开始播放等待音
try
{
_ = StartPlayingWaitingTone();
}
catch
{
// 忽略任何错误
}
}
}
private static async Task
PlayAudioAndWait(string audioPath, int? maxDuration = null, bool loop = false, CancellationToken token = default)
{
WaveOutEvent device = null;
AudioFileReader reader = null;
try
{
device = new WaveOutEvent();
reader = new AudioFileReader(audioPath);
device.Init(reader);
// 如果需要循环播放,创建一个循环播放的任务
if (loop && maxDuration.HasValue)
{
var startTime = DateTime.Now;
var endTime = startTime.AddMilliseconds(maxDuration.Value);
// 开始播放
device.Play();
// 循环播放直到达到指定时间
while (DateTime.Now < endTime && !token.IsCancellationRequested)
{
// 如果到达文件末尾,重新开始播放
if (reader.Position >= reader.Length)
{
reader.Position = 0;
}
// 如果没有在播放中,重新开始播放
if (device.PlaybackState != PlaybackState.Playing)
{
reader.Position = 0;
device.Play();
}
// 短暂等待避免CPU占用过高
await Task.Delay(50, token);
}
// 时间到,停止播放
device.Stop();
}
else if (maxDuration.HasValue)
{
// 开始播放
device.Play();
// 等待指定时间
await Task.Delay(maxDuration.Value, token);
// 时间到,停止播放
device.Stop();
}
else
{
// 创建一个TaskCompletionSource等待播放完成
var completionSource = new TaskCompletionSource<bool>();
// 一次性事件处理,播放完成后设置结果
EventHandler<StoppedEventArgs> handler = null;
handler = (s, e) =>
{
device.PlaybackStopped -= handler;
completionSource.TrySetResult(true);
};
device.PlaybackStopped += handler;
device.Play();
// 注册取消操作
using var registration = token.Register(() =>
{
device.Stop();
completionSource.TrySetCanceled(token);
});
// 等待播放完成或取消
await completionSource.Task;
}
}
catch (Exception ex)
{
if (token.IsCancellationRequested)
{
// 如果是取消引起的异常重新抛出OperationCanceledException
throw new OperationCanceledException("播放音频被用户取消", ex, token);
}
Console.WriteLine($"播放音频失败: {ex.Message}");
}
finally
{
device?.Stop();
device?.Dispose();
reader?.Dispose();
}
}
private static void DisposeAudioDevices()
{
waitingToneCts?.Cancel();
waitingToneCts?.Dispose();
waitingToneDevice?.Dispose();
waitingToneReader?.Dispose();
foreach (var (device, reader) in KeyToneDevices.Values)
{
device.Dispose();
reader.Dispose();
}
KeyToneDevices.Clear();
}
}

View File

@ -531,25 +531,6 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable
throw new OperationCanceledException("录音流程被用户取消");
}
}
// 播放提示用户录音的音频
Console.WriteLine("播放提示用户录音音频...");
try
{
await PlayAudioAndWait(
_config.AudioFiles.GetFullPath(_config.AudioFiles.PromptUserRecordFile),
null, false, recordingCts.Token);
}
catch (OperationCanceledException)
{
throw;
}
if (recordingCts.Token.IsCancellationRequested)
{
throw new OperationCanceledException("录音流程被用户取消");
}
// 开始实际录音逻辑
Console.WriteLine("开始初始化录音设备...");
@ -570,7 +551,27 @@ public class PhoneBoothService : IPhoneBoothService, IDisposable
Console.WriteLine("正在初始化录音设备...");
// 初始化录音设备
await StartAudioRecording(_recordingFilePath);
Console.WriteLine("正在录音中...");
Console.WriteLine("录音开始...");
if (recordingCts.Token.IsCancellationRequested)
{
throw new OperationCanceledException("录音流程被用户取消");
}
// 播放提示用户录音的音频
Console.WriteLine("播放提示用户录音音频...");
try
{
await PlayAudioAndWait(
_config.AudioFiles.GetFullPath(_config.AudioFiles.PromptUserRecordFile),
null, false, recordingCts.Token);
}
catch (OperationCanceledException)
{
throw;
}
if (recordingCts.Token.IsCancellationRequested)
{
throw new OperationCanceledException("录音流程被用户取消");

View File

@ -1,5 +1,5 @@
{
"SignalRHubUrl": "http://115.159.44.16/audiohub",
"SignalRHubUrl": "http://localhost:81/audiohub",
"ConfigBackupPath": "config.json",
"AutoConnectToServer": true,
"AllowOfflineStart": false

View File

@ -29,6 +29,9 @@ namespace ShengShengBuXi.Hubs
private readonly IAudioProcessingService _audioProcessingService;
private readonly IConfigurationService _configurationService;
private readonly ISpeechToTextService _speechToTextService;
/// <summary>
/// 客户端列表
/// </summary>
private static readonly ConcurrentDictionary<string, ClientInfo> _clients = new ConcurrentDictionary<string, ClientInfo>();
private readonly ILogger<AudioHub> _logger;
private readonly IHubContext<AudioHub> _hubContext;
@ -42,6 +45,8 @@ namespace ShengShengBuXi.Hubs
/// 用于清理过期记录的计时器
/// </summary>
private static Timer _cleanupTimer;
// 管理员配置保存路径
private static readonly string ConfigDirectory = Path.Combine(Directory.GetCurrentDirectory(), "config");
private static bool isInitialized = false;
@ -78,30 +83,48 @@ namespace ShengShengBuXi.Hubs
/// <summary>
/// 监控文本队列的持久化文件路径
/// </summary>
private static readonly string _monitorTextQueueFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config/monitor_text_queue.json");
private static readonly string _monitorTextQueueFilePath = Path.Combine(ConfigDirectory, "monitor_text_queue.json");
/// <summary>
/// 预设句子文件路径
/// </summary>
private static readonly string _sentencesFilePath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config/sentences.txt");
private static readonly string _sentencesFilePath = Path.Combine(ConfigDirectory, "sentences.txt");
/// <summary>
/// 真实用户显示记录的持久化文件路径
/// </summary>
private static readonly string _realUserDisplayLogsPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config/real_user_displays.log");
private static readonly string _realUserDisplayLogsPath = Path.Combine(ConfigDirectory, "real_user_displays.log");
/// <summary>
/// 预设句子列表
/// </summary>
private static List<string> _presetSentences = new List<string>();
/// <summary>
/// 预设句子2文件路径
/// </summary>
private static readonly string _sentences2FilePath = Path.Combine(ConfigDirectory, "sentences2.txt");
/// <summary>
/// 预设句子2列表
/// </summary>
private static List<string> _presetSentences2 = new List<string>();
/// <summary>
/// 当前使用的预设句子类型 (0: 默认预设句子, 1: 预设句子2)
/// </summary>
private static int _currentPresetSentenceType = 0;
/// <summary>
/// 预设句子类型配置文件路径
/// </summary>
private static readonly string _presetSentenceConfigPath = Path.Combine(ConfigDirectory, "newconfig.js");
// 配置信息
private static string _displayConfig = "{}";
private static object _displayConfigLock = new object();
// 控屏开关设置
private static bool _manualScreenControlEnabled = false;
private static readonly string _screenControlSettingPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "config/screen_control.json");
private static readonly string _screenControlSettingPath = Path.Combine(ConfigDirectory, "screen_control.json");
// 管理员配置保存路径
private static readonly string ConfigDirectory = Path.Combine(Directory.GetCurrentDirectory(), "config");
private static readonly string DisplayConfigPath = Path.Combine(ConfigDirectory, "display.json");
private static DateTime _lastRealTimeResultSentTime = DateTime.MinValue;
@ -129,6 +152,15 @@ namespace ShengShengBuXi.Hubs
// 加载控屏设置
LoadScreenControlSetting();
// 加载预设句子类型设置
LoadPresetSentenceTypeSetting();
// 加载预设句子
LoadPresetSentencesFromFile();
// 加载预设句子2
LoadPresetSentences2FromFile();
}
// 加载显示配置
@ -235,6 +267,51 @@ namespace ShengShengBuXi.Hubs
}
}
// 加载预设句子类型设置
private static void LoadPresetSentenceTypeSetting()
{
try
{
if (File.Exists(_presetSentenceConfigPath))
{
string json = File.ReadAllText(_presetSentenceConfigPath);
var setting = JsonConvert.DeserializeObject<dynamic>(json);
_currentPresetSentenceType = setting?.Type ?? 0;
Console.WriteLine($"加载预设句子类型设置成功,当前类型: {_currentPresetSentenceType}");
}
else
{
// 默认使用类型0
_currentPresetSentenceType = 0;
// 保存默认设置
SavePresetSentenceTypeSetting();
Console.WriteLine("创建默认预设句子类型设置成功");
}
}
catch (Exception ex)
{
Console.WriteLine($"加载预设句子类型设置出错: {ex.Message}");
_currentPresetSentenceType = 0;
}
}
// 保存预设句子类型设置
private static bool SavePresetSentenceTypeSetting()
{
try
{
string json = JsonConvert.SerializeObject(new { Type = _currentPresetSentenceType });
File.WriteAllText(_presetSentenceConfigPath, json);
return true;
}
catch (Exception ex)
{
Console.WriteLine($"保存预设句子类型设置出错: {ex.Message}");
return false;
}
}
/// <summary>
/// 初始化音频Hub
/// </summary>
@ -591,7 +668,8 @@ namespace ShengShengBuXi.Hubs
// 添加音频数据发送任务,使用单独的客户端连接
// 使用清晰的对象格式并添加WAV头以确保格式正确
var wavData = _audioProcessingService.AddWavHeader(dataToSend, config.SampleRate, config.Channels);
tasks.Add(Clients.Client(clientId).SendAsync("ReceiveAudioData", new {
tasks.Add(Clients.Client(clientId).SendAsync("ReceiveAudioData", new
{
format = "WAV",
sampleRate = config.SampleRate,
channels = config.Channels,
@ -841,6 +919,8 @@ namespace ShengShengBuXi.Hubs
Text = text,
Timestamp = DateTime.Now
});
_presetSentences.Add(text);
_presetSentences2.Add(text);
return false;
}
@ -1090,6 +1170,13 @@ namespace ShengShengBuXi.Hubs
return;
}
if (highestPriority.Value.Text.Contains("青竹园"))
{
if (_displayTextQueue.TryRemove(highestPriority.Key, out _))
{
}
return;
}
// 在手动控屏模式下,只处理真实用户的消息
if (_manualScreenControlEnabled && !highestPriority.Value.IsRealUser)
@ -1137,7 +1224,28 @@ namespace ShengShengBuXi.Hubs
{
try
{
// 确保预设句子列表不为空
// 根据当前预设句子类型选择使用哪个预设句子列表
List<string> currentPresetSentences;
if (_currentPresetSentenceType == 1)
{
// 使用预设句子2
if (_presetSentences2.Count == 0)
{
LoadPresetSentences2FromFile();
// 如果加载后仍然为空,使用默认句子
if (_presetSentences2.Count == 0)
{
_presetSentences2.Add("每一次跨越山川,都是为了与你重逢。");
_presetSentences2.Add("星光璀璨的夜晚,我想起了你的笑容。");
}
}
currentPresetSentences = _presetSentences2;
}
else
{
// 使用默认预设句子
if (_presetSentences.Count == 0)
{
LoadPresetSentencesFromFile();
@ -1149,7 +1257,11 @@ namespace ShengShengBuXi.Hubs
_presetSentences.Add("时光匆匆流逝,思念却越来越深。");
}
}
_presetSentences.OrderBy(it => Guid.NewGuid()).ToList().ForEach(item =>
currentPresetSentences = _presetSentences;
}
// 随机排序并添加到显示队列
currentPresetSentences.OrderBy(it => Guid.NewGuid()).ToList().ForEach(item =>
{
var displayText = new DisplayText
{
@ -1173,7 +1285,7 @@ namespace ShengShengBuXi.Hubs
/// <summary>
/// 从文件加载预设句子
/// </summary>
private void LoadPresetSentencesFromFile()
private static void LoadPresetSentencesFromFile()
{
try
{
@ -1182,7 +1294,7 @@ namespace ShengShengBuXi.Hubs
// 检查文件是否存在
if (!File.Exists(_sentencesFilePath))
{
_logger.LogWarning($"预设句子文件不存在: {_sentencesFilePath},将创建默认文件");
Console.WriteLine($"预设句子文件不存在: {_sentencesFilePath},将创建默认文件");
// 创建目录(如果不存在)
Directory.CreateDirectory(Path.GetDirectoryName(_sentencesFilePath));
@ -1204,11 +1316,53 @@ namespace ShengShengBuXi.Hubs
}
}
_presetSentences = _presetSentences.OrderBy(x => Guid.NewGuid()).ToList();
_logger.LogInformation($"成功从文件加载预设句子: {_presetSentences.Count} 条");
Console.WriteLine($"成功从文件加载预设句子: {_presetSentences.Count} 条");
}
catch (Exception ex)
{
_logger.LogError($"加载预设句子失败: {ex.Message}");
Console.WriteLine($"加载预设句子失败: {ex.Message}");
}
}
/// <summary>
/// 从文件加载预设句子2
/// </summary>
private static void LoadPresetSentences2FromFile()
{
try
{
_presetSentences2.Clear();
// 检查文件是否存在
if (!File.Exists(_sentences2FilePath))
{
Console.WriteLine($"预设句子2文件不存在: {_sentences2FilePath},将创建默认文件");
// 创建目录(如果不存在)
Directory.CreateDirectory(Path.GetDirectoryName(_sentences2FilePath));
// 写入默认的预设句子2
File.WriteAllLines(_sentences2FilePath, new string[] {
});
}
// 读取文件中的每一行作为一个预设句子
string[] lines = File.ReadAllLines(_sentences2FilePath);
foreach (string line in lines)
{
// 忽略空行
if (!string.IsNullOrWhiteSpace(line))
{
_presetSentences2.Add(line.Trim());
}
}
_presetSentences2 = _presetSentences2.OrderBy(x => Guid.NewGuid()).ToList();
Console.WriteLine($"成功从文件加载预设句子2: {_presetSentences2.Count} 条");
}
catch (Exception ex)
{
Console.WriteLine($"加载预设句子2失败: {ex.Message}");
}
}
@ -1833,5 +1987,148 @@ namespace ShengShengBuXi.Hubs
return records;
}
/// <summary>
/// 同时保存两个预设句子列表到文件
/// </summary>
private static void SavePresetSentencesToFile()
{
try
{
// 保存预设句子
File.WriteAllLines(_sentencesFilePath, _presetSentences);
Console.WriteLine($"成功保存预设句子到文件: {_presetSentences.Count} 条");
// 保存预设句子2
File.WriteAllLines(_sentences2FilePath, _presetSentences2);
Console.WriteLine($"成功保存预设句子2到文件: {_presetSentences2.Count} 条");
}
catch (Exception ex)
{
Console.WriteLine($"保存预设句子失败: {ex.Message}");
}
}
/// <summary>
/// 添加预设句子
/// </summary>
/// <param name="text">要添加的预设句子文本</param>
/// <returns>处理任务</returns>
public async Task<bool> AddPresetSentence(string text)
{
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
{
_logger.LogWarning($"未注册的客户端尝试添加预设句子: {Context.ConnectionId}");
await Clients.Caller.SendAsync("Error", "请先注册客户端");
return false;
}
if (clientInfo.ClientType != ClientType.Monitor && clientInfo.ClientType != ClientType.WebAdmin)
{
_logger.LogWarning($"非监控或管理端客户端尝试添加预设句子: {Context.ConnectionId}, 类型: {clientInfo.ClientType}");
await Clients.Caller.SendAsync("Error", "只有监控或管理端客户端可以添加预设句子");
return false;
}
if (string.IsNullOrWhiteSpace(text))
{
_logger.LogWarning("尝试添加空预设句子");
await Clients.Caller.SendAsync("Error", "预设句子不能为空");
return false;
}
_logger.LogInformation($"添加预设句子: {text}");
try
{
// 同时添加到两个列表,确保同步
_presetSentences.Add(text.Trim());
_presetSentences2.Add(text.Trim());
// 保存到文件
SavePresetSentencesToFile();
// 通知其他客户端预设句子已更新
await Clients.Groups(new[] { "webadmin", "monitor" })
.SendAsync("PresetSentencesUpdated");
return true;
}
catch (Exception ex)
{
_logger.LogError($"添加预设句子失败: {ex.Message}");
await Clients.Caller.SendAsync("Error", $"添加预设句子失败: {ex.Message}");
return false;
}
}
/// <summary>
/// 获取当前预设句子类型
/// </summary>
/// <returns>当前预设句子类型 (0: 默认, 1: 预设句子2)</returns>
public async Task<int> GetPresetSentenceType()
{
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
{
_logger.LogWarning($"未注册的客户端尝试获取预设句子类型: {Context.ConnectionId}");
throw new HubException("请先注册客户端");
}
_logger.LogInformation($"客户端获取当前预设句子类型: {Context.ConnectionId}, 类型: {clientInfo.ClientType}");
return _currentPresetSentenceType;
}
/// <summary>
/// 更新预设句子类型
/// </summary>
/// <param name="type">预设句子类型 (0: 默认, 1: 预设句子2)</param>
/// <returns>处理任务</returns>
public async Task<bool> UpdatePresetSentenceType(int type)
{
if (!_clients.TryGetValue(Context.ConnectionId, out var clientInfo))
{
_logger.LogWarning($"未注册的客户端尝试更新预设句子类型: {Context.ConnectionId}");
await Clients.Caller.SendAsync("Error", "请先注册客户端");
return false;
}
if (clientInfo.ClientType != ClientType.Monitor && clientInfo.ClientType != ClientType.WebAdmin)
{
_logger.LogWarning($"非监控或管理端客户端尝试更新预设句子类型: {Context.ConnectionId}, 类型: {clientInfo.ClientType}");
await Clients.Caller.SendAsync("Error", "只有监控或管理端客户端可以更新预设句子类型");
return false;
}
if (type < 0 || type > 1)
{
_logger.LogWarning($"客户端尝试设置无效的预设句子类型: {type}");
await Clients.Caller.SendAsync("Error", "无效的预设句子类型有效值为0或1");
return false;
}
_logger.LogInformation($"更新预设句子类型: {Context.ConnectionId}, 新类型: {type}");
// 更新配置
_currentPresetSentenceType = type;
// 保存设置
if (SavePresetSentenceTypeSetting())
{
_displayTextQueue.Clear();
// 通知其他客户端预设句子类型已更改
await Clients.Groups(new[] { "webadmin", "monitor" })
.SendAsync("PresetSentenceTypeChanged", type);
return true;
}
return false;
}
public async Task GetClientList()
{
var clients = _clients.Values.ToList();
await Clients.Caller.SendAsync("ClientList", clients);
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -20,7 +20,7 @@
</div>
<!-- 消息区域 -->
<div id="message-area"></div>
<div id="message-area" style="position: fixed; bottom: 20px; right: 20px; max-width: 350px; z-index: 9999;"></div>
<!-- 主要内容区域 -->
<div class="row">
@ -28,33 +28,43 @@
<div class="card">
<div class="card-body">
<ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item" role="presentation">
<!-- <li class="nav-item" role="presentation">
<button class="nav-link active" id="config-tab" data-bs-toggle="tab" data-bs-target="#config" type="button" role="tab" aria-controls="config" aria-selected="true">系统配置</button>
</li> -->
<li class="nav-item" role="presentation"></li>
<button class="nav-link" id="recordings-tab" data-bs-toggle="tab" data-bs-target="#recordings"
type="button" role="tab" aria-controls="recordings" aria-selected="false">用户留言</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="clients-tab" data-bs-toggle="tab" data-bs-target="#clients" type="button" role="tab" aria-controls="clients" aria-selected="false">客户端</button>
<button class="nav-link" id="clients-tab" data-bs-toggle="tab" data-bs-target="#clients"
type="button" role="tab" aria-controls="clients" aria-selected="false">客户端</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="recordings-tab" data-bs-toggle="tab" data-bs-target="#recordings" type="button" role="tab" aria-controls="recordings" aria-selected="false">录音管理</button>
</li>
<li class="nav-item" role="presentation">
<!-- <li class="nav-item" role="presentation">
<button class="nav-link" id="recognition-tab" data-bs-toggle="tab" data-bs-target="#recognition" type="button" role="tab" aria-controls="recognition" aria-selected="false">识别结果</button>
</li>-->
<li class="nav-item" role="presentation"></li>
<button class="nav-link" id="display-config-tab" data-bs-toggle="tab"
data-bs-target="#display-config" type="button" role="tab" aria-controls="display-config"
aria-selected="false">显示配置</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="display-config-tab" data-bs-toggle="tab" data-bs-target="#display-config" type="button" role="tab" aria-controls="display-config" aria-selected="false">显示配置</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="user-records-tab" data-bs-toggle="tab" data-bs-target="#user-records" type="button" role="tab" aria-controls="user-records" aria-selected="false">用户记录</button>
<button class="nav-link" id="user-records-tab" data-bs-toggle="tab"
data-bs-target="#user-records" type="button" role="tab" aria-controls="user-records"
aria-selected="false">用户记录</button>
</li>
</ul>
<div class="tab-content" id="myTabContent">
<!-- 系统配置 -->
<div class="tab-pane fade show active" id="config" role="tabpanel" aria-labelledby="config-tab">
<div class="tab-pane fade " id="config" role="tabpanel" aria-labelledby="config-tab">
<form id="config-form" class="mt-3">
<div class="d-flex justify-content-end mb-3">
<button type="button" class="btn btn-success me-2" onclick="saveConfig()">保存配置</button>
<button type="button" class="btn btn-primary me-2" onclick="getLatestConfig()">刷新配置</button>
<button type="button" class="btn btn-secondary" onclick="exportConfig()">导出配置</button>
<button type="button" class="btn btn-success me-2"
onclick="saveConfig()">保存配置</button>
<button type="button" class="btn btn-primary me-2"
onclick="getLatestConfig()">刷新配置</button>
<button type="button" class="btn btn-secondary"
onclick="exportConfig()">导出配置</button>
</div>
<div class="row">
@ -63,44 +73,53 @@
<h5>控制台配置</h5>
<div class="mb-3">
<label for="serverUrl" class="form-label">服务器地址</label>
<input type="text" class="form-control" id="serverUrl" name="network.serverUrl">
<input type="text" class="form-control" id="serverUrl"
name="network.serverUrl">
<div class="form-text">WebSocket服务器地址</div>
</div>
<div class="mb-3">
<label for="reconnectAttempts" class="form-label">重连尝试次数</label>
<input type="number" class="form-control" id="reconnectAttempts" name="network.reconnectAttempts">
<input type="number" class="form-control" id="reconnectAttempts"
name="network.reconnectAttempts">
</div>
<div class="mb-3">
<label for="reconnectDelayMs" class="form-label">重连延迟(毫秒)</label>
<input type="number" class="form-control" id="reconnectDelayMs" name="network.reconnectDelayMs">
<input type="number" class="form-control" id="reconnectDelayMs"
name="network.reconnectDelayMs">
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="enableSpeechToText" name="network.enableSpeechToText">
<input type="checkbox" class="form-check-input" id="enableSpeechToText"
name="network.enableSpeechToText">
<label class="form-check-label" for="enableSpeechToText">启用语音识别</label>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="enableAudioStreaming" name="network.enableAudioStreaming">
<input type="checkbox" class="form-check-input" id="enableAudioStreaming"
name="network.enableAudioStreaming">
<label class="form-check-label" for="enableAudioStreaming">启用音频流传输</label>
</div>
<div class="mb-3">
<label for="heartbeatIntervalSeconds" class="form-label">心跳间隔(秒)</label>
<input type="number" class="form-control" id="heartbeatIntervalSeconds" name="network.heartbeatIntervalSeconds">
<input type="number" class="form-control" id="heartbeatIntervalSeconds"
name="network.heartbeatIntervalSeconds">
</div>
<h5 class="mt-4">拨号配置</h5>
<div class="mb-3">
<label for="minDigitsToDialOut" class="form-label">拨号最小位数</label>
<input type="number" class="form-control" id="minDigitsToDialOut" name="dial.minDigitsToDialOut">
<input type="number" class="form-control" id="minDigitsToDialOut"
name="dial.minDigitsToDialOut">
<div class="form-text">拨号所需的最小位数</div>
</div>
<div class="mb-3">
<label for="autoDialOutAfterSeconds" class="form-label">自动拨出等待时间(秒)</label>
<input type="number" class="form-control" id="autoDialOutAfterSeconds" name="dial.autoDialOutAfterSeconds">
<input type="number" class="form-control" id="autoDialOutAfterSeconds"
name="dial.autoDialOutAfterSeconds">
<div class="form-text">无新按键后自动拨出的等待秒数</div>
</div>
<div class="mb-3">
<label for="resetTimeoutSeconds" class="form-label">重置超时时间(秒)</label>
<input type="number" class="form-control" id="resetTimeoutSeconds" name="dial.resetTimeoutSeconds">
<input type="number" class="form-control" id="resetTimeoutSeconds"
name="dial.resetTimeoutSeconds">
<div class="form-text">位数不足时,无操作重置的等待秒数</div>
</div>
</div>
@ -110,57 +129,72 @@
<h5>录音配置</h5>
<div class="mb-3">
<label for="recordingFolder" class="form-label">录音保存文件夹</label>
<input type="text" class="form-control" id="recordingFolder" name="recording.recordingFolder">
<input type="text" class="form-control" id="recordingFolder"
name="recording.recordingFolder">
</div>
<div class="mb-3">
<label for="recordingDeviceNumber" class="form-label">录音设备编号</label>
<input type="number" class="form-control" id="recordingDeviceNumber" name="recording.recordingDeviceNumber">
<input type="number" class="form-control" id="recordingDeviceNumber"
name="recording.recordingDeviceNumber">
</div>
<div class="mb-3">
<label for="sampleRate" class="form-label">采样率</label>
<input type="number" class="form-control" id="sampleRate" name="recording.sampleRate">
<input type="number" class="form-control" id="sampleRate"
name="recording.sampleRate">
</div>
<div class="mb-3">
<label for="channels" class="form-label">声道数</label>
<input type="number" class="form-control" id="channels" name="recording.channels">
<input type="number" class="form-control" id="channels"
name="recording.channels">
<div class="form-text">1=单声道2=立体声</div>
</div>
<div class="mb-3">
<label for="bufferMilliseconds" class="form-label">缓冲区大小(毫秒)</label>
<input type="number" class="form-control" id="bufferMilliseconds" name="recording.bufferMilliseconds">
<input type="number" class="form-control" id="bufferMilliseconds"
name="recording.bufferMilliseconds">
</div>
<div class="mb-3">
<label for="silenceThreshold" class="form-label">静音检测阈值</label>
<input type="number" class="form-control" id="silenceThreshold" name="recording.silenceThreshold" step="0.01">
<input type="number" class="form-control" id="silenceThreshold"
name="recording.silenceThreshold" step="0.01">
</div>
<div class="mb-3">
<label for="silenceTimeoutSeconds" class="form-label">静音超时时间(秒)</label>
<input type="number" class="form-control" id="silenceTimeoutSeconds" name="recording.silenceTimeoutSeconds">
<input type="number" class="form-control" id="silenceTimeoutSeconds"
name="recording.silenceTimeoutSeconds">
<div class="form-text">无声音自动挂断的时间</div>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="allowUserHangup" name="recording.allowUserHangup">
<input type="checkbox" class="form-check-input" id="allowUserHangup"
name="recording.allowUserHangup">
<label class="form-check-label" for="allowUserHangup">允许用户手动挂断</label>
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="uploadRecordingToServer" name="recording.uploadRecordingToServer">
<label class="form-check-label" for="uploadRecordingToServer">上传录音文件到服务器</label>
<input type="checkbox" class="form-check-input" id="uploadRecordingToServer"
name="recording.uploadRecordingToServer">
<label class="form-check-label"
for="uploadRecordingToServer">上传录音文件到服务器</label>
</div>
<div class="mb-3">
<label for="recordingsFolder" class="form-label">服务器录音路径</label>
<input type="text" class="form-control" id="recordingsFolder" name="recording.recordingsFolder">
<input type="text" class="form-control" id="recordingsFolder"
name="recording.recordingsFolder">
</div>
<div class="mb-3">
<label for="fileNameFormat" class="form-label">录音文件名格式</label>
<input type="text" class="form-control" id="fileNameFormat" name="recording.fileNameFormat">
<input type="text" class="form-control" id="fileNameFormat"
name="recording.fileNameFormat">
</div>
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="autoCleanupOldRecordings" name="recording.autoCleanupOldRecordings">
<label class="form-check-label" for="autoCleanupOldRecordings">自动清理旧录音</label>
<input type="checkbox" class="form-check-input"
id="autoCleanupOldRecordings" name="recording.autoCleanupOldRecordings">
<label class="form-check-label"
for="autoCleanupOldRecordings">自动清理旧录音</label>
</div>
<div class="mb-3">
<label for="keepRecordingsDays" class="form-label">保留录音天数</label>
<input type="number" class="form-control" id="keepRecordingsDays" name="recording.keepRecordingsDays">
<input type="number" class="form-control" id="keepRecordingsDays"
name="recording.keepRecordingsDays">
<div class="form-text">启用自动清理时有效</div>
</div>
</div>
@ -172,31 +206,38 @@
<h5>音频文件配置</h5>
<div class="mb-3">
<label for="audioBasePath" class="form-label">音频文件基础路径</label>
<input type="text" class="form-control" id="audioBasePath" name="audioFiles.audioBasePath">
<input type="text" class="form-control" id="audioBasePath"
name="audioFiles.audioBasePath">
</div>
<div class="mb-3">
<label for="waitingToneFile" class="form-label">等待嘟音文件</label>
<input type="text" class="form-control" id="waitingToneFile" name="audioFiles.waitingToneFile">
<input type="text" class="form-control" id="waitingToneFile"
name="audioFiles.waitingToneFile">
</div>
<div class="mb-3">
<label for="waitForPickupFile" class="form-label">等待接电话音频</label>
<input type="text" class="form-control" id="waitForPickupFile" name="audioFiles.waitForPickupFile">
<input type="text" class="form-control" id="waitForPickupFile"
name="audioFiles.waitForPickupFile">
</div>
<div class="mb-3">
<label for="phonePickupFile" class="form-label">电话接起音频</label>
<input type="text" class="form-control" id="phonePickupFile" name="audioFiles.phonePickupFile">
<input type="text" class="form-control" id="phonePickupFile"
name="audioFiles.phonePickupFile">
</div>
<div class="mb-3">
<label for="promptUserRecordFile" class="form-label">提示用户录音音频</label>
<input type="text" class="form-control" id="promptUserRecordFile" name="audioFiles.promptUserRecordFile">
<input type="text" class="form-control" id="promptUserRecordFile"
name="audioFiles.promptUserRecordFile">
</div>
<div class="mb-3">
<label for="beepPromptFile" class="form-label">提示音文件</label>
<input type="text" class="form-control" id="beepPromptFile" name="audioFiles.beepPromptFile">
<input type="text" class="form-control" id="beepPromptFile"
name="audioFiles.beepPromptFile">
</div>
<div class="mb-3">
<label for="digitToneFileTemplate" class="form-label">数字按键音频模板</label>
<input type="text" class="form-control" id="digitToneFileTemplate" name="audioFiles.digitToneFileTemplate">
<input type="text" class="form-control" id="digitToneFileTemplate"
name="audioFiles.digitToneFileTemplate">
<div class="form-text">例如: {0}.mp3用于数字0-9的按键音</div>
</div>
</div>
@ -206,17 +247,20 @@
<h5>电话流程配置</h5>
<div class="mb-3">
<label for="waitForPickupMinSeconds" class="form-label">等待接听最小时间(秒)</label>
<input type="number" class="form-control" id="waitForPickupMinSeconds" name="callFlow.waitForPickupMinSeconds">
<input type="number" class="form-control" id="waitForPickupMinSeconds"
name="callFlow.waitForPickupMinSeconds">
<div class="form-text">等待接电话音频的最小持续时间</div>
</div>
<div class="mb-3">
<label for="waitForPickupMaxSeconds" class="form-label">等待接听最大时间(秒)</label>
<input type="number" class="form-control" id="waitForPickupMaxSeconds" name="callFlow.waitForPickupMaxSeconds">
<input type="number" class="form-control" id="waitForPickupMaxSeconds"
name="callFlow.waitForPickupMaxSeconds">
<div class="form-text">等待接电话音频的最大持续时间</div>
</div>
<div class="mb-3">
<label for="playPickupProbability" class="form-label">接听概率</label>
<input type="number" class="form-control" id="playPickupProbability" name="callFlow.playPickupProbability" step="0.01" min="0" max="1">
<input type="number" class="form-control" id="playPickupProbability"
name="callFlow.playPickupProbability" step="0.01" min="0" max="1">
<div class="form-text">播放电话接起音频的概率0-1之间</div>
</div>
</div>
@ -225,7 +269,7 @@
</div>
<!-- 客户端管理 -->
<div class="tab-pane fade" id="clients" role="tabpanel" aria-labelledby="clients-tab">
<div class="tab-pane fade" show id="clients" role="tabpanel" aria-labelledby="clients-tab">
<div class="mt-3">
<table class="table table-striped">
<thead>
@ -247,15 +291,16 @@
</div>
<!-- 录音管理 -->
<div class="tab-pane fade" id="recordings" role="tabpanel" aria-labelledby="recordings-tab">
<div class="tab-pane fade show active" id="recordings" role="tabpanel" aria-labelledby="recordings-tab">
<div class="mt-3">
<button class="btn btn-primary mb-3" onclick="getRecentRecordings()">刷新录音列表</button>
<!-- 音频播放器 -->
<!-- 音频播放器(将不再全局显示,而是嵌入到表格行中) -->
<div class="card mb-3" id="audioPlayerCard" style="display: none;">
<div class="card-header d-flex justify-content-between">
<span id="currentPlayingFile">当前播放:</span>
<button class="btn btn-sm btn-outline-secondary" onclick="closeAudioPlayer()">关闭</button>
<button class="btn btn-sm btn-outline-secondary"
onclick="closeAudioPlayer()">关闭</button>
</div>
<div class="card-body">
<audio id="audioPlayer" controls class="w-100">
@ -302,12 +347,16 @@
</div>
<!-- 显示配置 -->
<div class="tab-pane fade" id="display-config" role="tabpanel" aria-labelledby="display-config-tab">
<div class="tab-pane fade" id="display-config" role="tabpanel"
aria-labelledby="display-config-tab">
<div class="mt-3">
<div class="d-flex justify-content-end mb-3">
<button type="button" class="btn btn-success me-2" onclick="saveDisplayConfig()">保存配置</button>
<button type="button" class="btn btn-primary me-2" onclick="getDisplayConfig()">刷新配置</button>
<button type="button" class="btn btn-secondary" onclick="resetDisplayConfig()">重置默认值</button>
<button type="button" class="btn btn-success me-2"
onclick="saveDisplayConfig()">保存配置</button>
<button type="button" class="btn btn-primary me-2"
onclick="getDisplayConfig()">刷新配置</button>
<button type="button" class="btn btn-secondary"
onclick="resetDisplayConfig()">重置默认值</button>
</div>
<form id="display-config-form" class="mt-3">
@ -320,18 +369,24 @@
</div>
<div class="card-body">
<div class="mb-3">
<label for="leftTurnPageHeight" class="form-label">翻页高度比例</label>
<input type="number" class="form-control" id="leftTurnPageHeight" step="0.1" min="0.1" max="1.0" value="0.8">
<label for="leftTurnPageHeight"
class="form-label">翻页高度比例</label>
<input type="number" class="form-control"
id="leftTurnPageHeight" step="0.1" min="0.1" max="1.0"
value="0.8">
<div class="form-text">页面高度比例值范围0.1-1.0</div>
</div>
<div class="mb-3">
<label for="leftFontSize" class="form-label">字体大小</label>
<input type="text" class="form-control" id="leftFontSize" value="16px">
<input type="text" class="form-control" id="leftFontSize"
value="16px">
<div class="form-text">左侧历史记录字体大小例如16px</div>
</div>
<div class="mb-3">
<label for="leftTypewriterSpeed" class="form-label">打字效果速度</label>
<input type="number" class="form-control" id="leftTypewriterSpeed" min="10" max="500" value="50">
<label for="leftTypewriterSpeed"
class="form-label">打字效果速度</label>
<input type="number" class="form-control"
id="leftTypewriterSpeed" min="10" max="500" value="50">
<div class="form-text">数值越小,打字效果速度越快</div>
</div>
</div>
@ -347,7 +402,8 @@
<div class="card-body">
<div class="mb-3">
<label for="rightFontSize" class="form-label">字体大小</label>
<input type="text" class="form-control" id="rightFontSize" value="24px">
<input type="text" class="form-control" id="rightFontSize"
value="24px">
<div class="form-text">右侧主要显示文本的字体大小</div>
</div>
<div class="mb-3">
@ -367,8 +423,10 @@
</select>
</div>
<div class="mb-3">
<label for="rightTypewriterSpeed" class="form-label">打字效果速度</label>
<input type="number" class="form-control" id="rightTypewriterSpeed" min="10" max="500" value="50">
<label for="rightTypewriterSpeed"
class="form-label">打字效果速度</label>
<input type="number" class="form-control"
id="rightTypewriterSpeed" min="10" max="500" value="50">
<div class="form-text">数值越小,打字效果速度越快</div>
</div>
</div>
@ -383,37 +441,51 @@
</div>
<div class="card-body">
<div class="mb-3 form-check">
<input type="checkbox" class="form-check-input" id="waterEffectEnabled" checked>
<label class="form-check-label" for="waterEffectEnabled">启用水波纹效果</label>
<input type="checkbox" class="form-check-input"
id="waterEffectEnabled" checked>
<label class="form-check-label"
for="waterEffectEnabled">启用水波纹效果</label>
</div>
<div class="mb-3">
<label for="waterMinInterval" class="form-label">最小间隔(毫秒)</label>
<input type="number" class="form-control" id="waterMinInterval" min="100" max="5000" value="800">
<label for="waterMinInterval"
class="form-label">最小间隔(毫秒)</label>
<input type="number" class="form-control" id="waterMinInterval"
min="100" max="5000" value="800">
</div>
<div class="mb-3">
<label for="waterMaxInterval" class="form-label">最大间隔(毫秒)</label>
<input type="number" class="form-control" id="waterMaxInterval" min="100" max="10000" value="2000">
<label for="waterMaxInterval"
class="form-label">最大间隔(毫秒)</label>
<input type="number" class="form-control" id="waterMaxInterval"
min="100" max="10000" value="2000">
</div>
<div class="mb-3">
<label for="waterSimultaneousDrops" class="form-label">同时涟漪数量</label>
<input type="number" class="form-control" id="waterSimultaneousDrops" min="1" max="10" value="3">
<label for="waterSimultaneousDrops"
class="form-label">同时涟漪数量</label>
<input type="number" class="form-control"
id="waterSimultaneousDrops" min="1" max="10" value="3">
</div>
<div class="mb-3">
<label for="waterFadeOutSpeed" class="form-label">淡出速度</label>
<input type="number" class="form-control" id="waterFadeOutSpeed" min="0.01" max="1" step="0.01" value="0.1">
<input type="number" class="form-control" id="waterFadeOutSpeed"
min="0.01" max="1" step="0.01" value="0.1">
</div>
<div class="mb-3">
<label for="waterCenterBias" class="form-label">中心倾向</label>
<input type="number" class="form-control" id="waterCenterBias" min="0" max="1" step="0.1" value="0.5">
<input type="number" class="form-control" id="waterCenterBias"
min="0" max="1" step="0.1" value="0.5">
<div class="form-text">0表示随机1表示集中在中心</div>
</div>
<div class="mb-3">
<label for="waterLargeDropProbability" class="form-label">大水滴概率</label>
<input type="number" class="form-control" id="waterLargeDropProbability" min="0" max="1" step="0.1" value="0.2">
<label for="waterLargeDropProbability"
class="form-label">大水滴概率</label>
<input type="number" class="form-control"
id="waterLargeDropProbability" min="0" max="1" step="0.1"
value="0.2">
</div>
<div class="mb-3">
<label for="waterLargeDropSize" class="form-label">大水滴大小</label>
<input type="number" class="form-control" id="waterLargeDropSize" min="3" max="20" value="9">
<input type="number" class="form-control"
id="waterLargeDropSize" min="3" max="20" value="9">
</div>
</div>
</div>
@ -427,7 +499,8 @@
<div class="tab-pane fade" id="user-records" role="tabpanel" aria-labelledby="user-records-tab">
<div class="mt-3">
<div class="d-flex justify-content-end mb-3">
<button type="button" class="btn btn-primary" onclick="getRealUserRecords()">刷新记录</button>
<button type="button" class="btn btn-primary"
onclick="getRealUserRecords()">刷新记录</button>
</div>
<div class="table-responsive">
@ -493,7 +566,8 @@
function showMessage(message, type = 'info') {
const messageArea = document.getElementById("message-area");
const alert = document.createElement("div");
alert.className = `alert alert-${type} alert-dismissible fade show`;
alert.className = `alert alert-${type} alert-dismissible fade show mb-2`;
alert.style.boxShadow = "0 4px 8px rgba(0,0,0,0.1)";
alert.innerHTML = `
${message}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
@ -544,6 +618,9 @@
log("SignalR连接成功");
updateConnectionStatus("已连接", "success");
// 设置SignalR事件处理器
setupSignalRHandlers();
// 注册为管理员客户端
connection.invoke("RegisterClient", 1, "WebAdmin")
.then(function () {
@ -553,7 +630,7 @@
// 获取显示配置
getDisplayConfig();
// 获取客户端列表
//getClientList();
getClientList();
setTimeout(getRecentRecordings, 1000);
setTimeout(getRealUserRecords, 1500);
})
@ -1023,41 +1100,83 @@
}
// 在页面中播放录音
function playRecordingInPage(fileName) {
const audioPlayer = document.getElementById("audioPlayer");
const audioPlayerCard = document.getElementById("audioPlayerCard");
const currentPlayingFile = document.getElementById("currentPlayingFile");
function playRecordingInPage(fileName, rowElement) {
// 关闭任何已打开的播放器
closeAudioPlayer();
// 设置音频源
audioPlayer.src = `/recordings/${fileName}`;
// 创建一个新的播放器行
const playerRow = document.createElement("tr");
playerRow.id = "inline-player-row";
playerRow.className = "audio-player-row";
// 更新显示信息
currentPlayingFile.textContent = `当前播放:${fileName}`;
// 创建带有播放器的单元格
const playerCell = document.createElement("td");
playerCell.colSpan = 5;
playerCell.className = "p-0";
// 显示播放器
audioPlayerCard.style.display = "block";
// 创建播放器卡片
const playerCard = document.createElement("div");
playerCard.className = "card";
playerCard.style.border = "none";
playerCard.style.borderRadius = "0";
playerCard.style.boxShadow = "inset 0 3px 6px rgba(0,0,0,0.1)";
playerCard.style.backgroundColor = "#f8f9fa";
// 开始播放
audioPlayer.play().catch(err => {
log("播放失败: " + err);
showMessage("播放失败: " + err, "danger");
});
// 创建卡片内容
playerCard.innerHTML = `
<div class="card-body py-2">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="text-muted">正在播放:${fileName}</span>
<button class="btn btn-sm btn-outline-secondary" onclick="closeAudioPlayer()">关闭</button>
</div>
<audio controls class="w-100" autoplay>
<source src="/recordings/${fileName}" type="audio/wav">
您的浏览器不支持音频播放器
</audio>
</div>
`;
// 滚动到播放器位置
audioPlayerCard.scrollIntoView({ behavior: 'smooth' });
// 组装播放器行
playerCell.appendChild(playerCard);
playerRow.appendChild(playerCell);
// 插入到表格中点击的行的后面
const targetRow = rowElement.closest('tr');
if (targetRow && targetRow.parentNode) {
targetRow.parentNode.insertBefore(playerRow, targetRow.nextSibling);
// 将选中行标记为活动
targetRow.classList.add("table-primary");
}
}
// 关闭音频播放器
function closeAudioPlayer() {
const audioPlayer = document.getElementById("audioPlayer");
const audioPlayerCard = document.getElementById("audioPlayerCard");
// 移除任何内联播放器行
const playerRow = document.getElementById("inline-player-row");
if (playerRow) {
// 找到激活的行并移除高亮
const activeRow = document.querySelector("#recording-list tr.table-primary");
if (activeRow) {
activeRow.classList.remove("table-primary");
}
// 暂停播放
// 移除播放器行
playerRow.remove();
}
// 隐藏原来的顶部播放器(兼容性保留)
const audioPlayerCard = document.getElementById("audioPlayerCard");
if (audioPlayerCard) {
audioPlayerCard.style.display = "none";
// 停止原播放器
const audioPlayer = document.getElementById("audioPlayer");
if (audioPlayer) {
audioPlayer.pause();
audioPlayer.src = '';
// 隐藏播放器
audioPlayerCard.style.display = "none";
}
}
}
// 设置SignalR事件处理程序
@ -1345,7 +1464,7 @@
<td>获取中...</td>
<td>${createDate}</td>
<td>
<button class="btn btn-sm btn-primary" onclick="playRecordingInPage('${fileName}')">播放</button>
<button class="btn btn-sm btn-primary" onclick="playRecordingInPage('${fileName}', this)">播放</button>
<a href="/recordings/${fileName}" class="btn btn-sm btn-secondary" download>下载</a>
</td>
`;
@ -1626,5 +1745,32 @@
// 初始化显示配置默认值
resetDisplayConfig();
});
// 获取客户端列表
function getClientList() {
if (!connection || connection.state !== signalR.HubConnectionState.Connected) {
showMessage("无法获取客户端列表:未连接到服务器", "warning");
return;
}
log("正在获取客户端列表...");
connection.invoke("GetClientList")
.then(() => {
log("已成功发送获取客户端列表请求");
})
.catch(err => {
log("获取客户端列表失败: " + err);
showMessage("获取客户端列表失败: " + err, "danger");
});
}
// 切换到客户端标签页时自动刷新客户端列表
document.getElementById('clients-tab').addEventListener('click', function () {
if (connection && connection.state === signalR.HubConnectionState.Connected) {
log("切换到客户端标签页,自动刷新客户端列表");
setTimeout(getClientList, 500);
}
});
</script>
}

View File

@ -316,7 +316,7 @@
// 整条文字渐显效果
newP.animate(
{ opacity: 1 },
2000, // 设置动画时长为2秒
3000, // 设置动画时长为2秒
'swing'
);
return true;

View File

@ -5,7 +5,36 @@
}
<!-- 添加Bootstrap Icons库的引用 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.10.5/font/bootstrap-icons.css">
<link rel="stylesheet" href="~/css/bootstrap-icons.css">
<style>
/* 覆盖Bootstrap默认的active类样式 */
#monitor-text-list .list-group-item.active {
background-color: transparent !important; /* 透明背景 */
color: inherit !important; /* 继承原有文字颜色 */
border-color: #dee2e6 !important; /* 使用默认边框颜色 */
}
/* 如果需要,可以添加其他标识选中项的样式 */
#monitor-text-list .list-group-item.active {
border-left: 3px solid #28a745 !important; /* 添加左侧绿色边框作为选中标识 */
font-weight: bold; /* 文字加粗 */
}
/* 视频播放器样式 */
.audio-player-container {
display: none;
margin-top: 8px;
width: 100%;
}
.audio-player {
width: 100%;
height: 40px;
background-color: #f8f9fa;
border-radius: 4px;
}
</style>
<div class="container-fluid">
<!-- 头部区域 (100%, 20%) -->
@ -25,18 +54,30 @@
<span id="status-text" class="me-3">未检测到通话</span>
</div>
<!-- 单选按钮组 - 隐藏自动识别显示选项 -->
<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>
<input type="radio" class="btn-check" name="displayMode" id="displayMode1" value="1">
<label class="btn btn-outline-primary" for="displayMode1">手动处理显示</label>
<!-- 预设句子类型控制 -->
<div class="btn-group ms-3" role="group">
<span class="me-2 d-flex align-items-center">大屏文本:</span>
<input type="radio" class="btn-check" name="presetSentenceType" id="presetSentenceType0"
value="0" checked onclick="updatePresetSentenceType(0)">
<label class="btn btn-outline-primary" for="presetSentenceType0">文本1</label>
<input type="radio" class="btn-check" name="presetSentenceType" id="presetSentenceType1"
value="1" onclick="updatePresetSentenceType(1)">
<label class="btn btn-outline-primary" for="presetSentenceType1">文本2</label>
</div>
<!-- 音频传输开关 - 隐藏关闭音频传输选项 -->
<!-- 控屏开关 - 优化样式 -->
<div class="btn-group ms-3" role="group">
<span class="me-2 d-flex align-items-center">控屏模式:</span>
<input type="radio" class="btn-check" name="screenControl" id="screenControlAuto" value="0"
checked>
<label class="btn btn-outline-primary" for="screenControlAuto">自动</label>
<input type="radio" class="btn-check" name="screenControl" id="screenControlManual"
value="1">
<label class="btn btn-outline-primary" for="screenControlManual">手动</label>
</div>
<!-- 音频传输开关 - 隐藏 -->
<div class="btn-group ms-3" role="group" style="display: none;">
<input type="radio" class="btn-check" name="audioStreaming" id="audioStreaming1" value="1"
checked>
<label class="btn btn-outline-success" for="audioStreaming1">开启音频传输</label>
@ -45,25 +86,6 @@
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 class="btn-group ms-3" role="group">
控评开关:
<input type="radio" class="btn-check" name="screenControl" id="screenControlAuto" value="0"
checked>
<label class="btn btn-outline-primary" for="screenControlAuto">自动</label>
<input type="radio" class="btn-check" name="screenControl" id="screenControlManual"
value="1">
<label class="btn btn-outline-primary" for="screenControlManual">手动</label>
</div>
</div>
</div>
@ -76,7 +98,7 @@
<div class="col-3">
<div class="card h-100">
<div class="card-header">
<h5 class="mb-0">监控文本列表</h5>
<h5 class="mb-0">语音列表</h5>
</div>
<div class="card-body p-0">
<div id="monitor-text-list" class="list-group list-group-flush"
@ -115,12 +137,12 @@
<div class="card h-100">
<div class="card-header">
<h5 class="mb-0">
文本编辑 <span style="color:#6c757d;font-size:12px;">(最多输入100个文字)</span>
文本编辑
</h5>
</div>
<div class="card-body">
<textarea id="text-input" class="form-control h-100" placeholder="请输入要显示的文本..."
maxlength="100"></textarea>
<textarea id="text-input" class="form-control h-100"
placeholder="请输入要显示的文本..."></textarea>
</div>
</div>
@ -169,6 +191,8 @@
</div>
</div>
<!-- 调试区域 (底部) -->
<div class="row mt-3">
<div class="col-12">
@ -217,35 +241,6 @@
</div>
</div>
<div class="card mb-3">
<div class="card-header">
<h5>音频控制</h5>
</div>
<div class="card-body">
<div class="form-check form-switch mb-3">
<input class="form-check-input" type="checkbox" id="audioStreamToggle">
<label class="form-check-label" for="audioStreamToggle">音频流接收</label>
</div>
<!-- 音量控制 -->
<div class="mb-3">
<label for="volumeSlider" class="form-label">音量控制 <span id="volumeDisplay">100%</span></label>
<div class="d-flex align-items-center">
<i class="bi bi-volume-down me-2"></i>
<input type="range" class="form-range flex-grow-1" id="volumeSlider" min="0" max="100" value="100">
<i class="bi bi-volume-up ms-2"></i>
</div>
<small class="text-muted">音量已增强3倍可按需调整</small>
</div>
<div class="mt-3" id="callStatus">
<div class="alert alert-secondary">
未检测到通话
</div>
</div>
</div>
</div>
@section Scripts {
<script src="~/lib/microsoft-signalr/signalr.min.js"></script>
@ -434,6 +429,9 @@
// 获取当前控屏设置
getServerScreenControlSetting();
// 获取当前预设句子类型设置
getServerPresetSentenceTypeSetting();
// 设置定时刷新显示列表
if (refreshDisplayInterval) {
clearInterval(refreshDisplayInterval);
@ -497,7 +495,7 @@
showFinalTextResult(result);
// 同时添加到监控列表的顶部(如果不存在于列表中)
addToMonitorList(result);
// addToMonitorList(result);
});
// 显示模式更新消息
@ -517,13 +515,32 @@
}
});
// 通话状态改变
connection.on("CallStateChanged", (isActive) => {
updateCallStatus(isActive);
});
// 接收实时音频数据
connection.on("ReceiveAudioData", (audioData) => {
if (!isActiveShow) {
// 添加音频流持续检测逻辑
if (!window.audioStreamStartTime) {
// 设置音频流开始接收时间
window.audioStreamStartTime = Date.now();
log("检测到音频流,开始计时");
} else {
// 检查是否超过5秒
const elapsedTime = Date.now() - window.audioStreamStartTime;
if (elapsedTime > 5000) {
log(`音频流持续时间 ${(elapsedTime / 1000).toFixed(1)}秒,触发通话状态更新`);
window.audioStreamStartTime = null; // 重置计时器
updateCallStatus(true);
return;
}
}
return;
} else {
// 重置音频流计时器
window.audioStreamStartTime = null;
}
});
// 音频流设置更新消息
@ -568,21 +585,33 @@
// 当检测到新通话时,先显示确认对话框
const confirmDialog = new bootstrap.Modal(document.getElementById('callConfirmDialog'));
confirmDialog.show();
enterRoom();
return; // 等待用户确认后再继续处理
} else {
const indicator = document.getElementById("status-indicator");
const statusText = document.getElementById("status-text");
indicator.style.backgroundColor = "red";
statusText.textContent = "未检测到通话";
leaveRoom();
// 如果确认对话框还在显示(用户未点击接听),则自动关闭
// const confirmDialog = new bootstrap.Modal(document.getElementById('callConfirmDialog'));
const confirmDialog = bootstrap.Modal.getInstance(document.getElementById('callConfirmDialog'));
if (confirmDialog) {
confirmDialog.hide();
exitRoom();
// 添加以下代码来移除背景遮罩
setTimeout(() => {
const backdrop = document.querySelector('.modal-backdrop');
if (backdrop) {
backdrop.remove();
}
// 或者还需要移除body上的class
document.body.classList.remove('modal-open');
document.body.style.overflow = '';
document.body.style.paddingRight = '';
}, 150); // 给一点延迟,确保模态框动画完成
log("通话已结束,自动关闭确认对话框");
}
}
// 有新通话时刷新监控列表
@ -596,36 +625,12 @@
indicator.style.backgroundColor = "green";
statusText.textContent = "正在通话中";
callInProgress = true;
// 初始化音频上下文(如果需要且启用了音频流)
if (isAudioStreamEnabled) {
// 如果存在音频上下文,则先尝试关闭,然后重新创建
if (audioContext) {
log("重置音频上下文以确保新的通话正常播放");
try {
// 释放旧的增益节点
if (audioGainNode) {
audioGainNode.disconnect();
audioGainNode = null;
}
// 关闭旧的音频上下文
audioContext.close().catch(e => log("关闭音频上下文失败: " + e));
audioContext = null;
} catch (e) {
log("重置音频上下文失败: " + e);
}
}
// 创建新的音频上下文
initAudioContext();
}
enterRoom();
// 关闭确认对话框
const confirmDialog = bootstrap.Modal.getInstance(document.getElementById('callConfirmDialog'));
if (confirmDialog) {
confirmDialog.hide();
}
// 刷新监控列表
loadMonitorTextList();
}
@ -668,7 +673,6 @@
const listItem = document.createElement("div");
listItem.className = "list-group-item";
listItem.dataset.id = item.id;
// 存储原始文本,用于点击后显示
listItem.dataset.fullText = item.text || item.completedText || "无文本内容";
// 添加点击事件
@ -699,11 +703,12 @@
// 截取前10个字符若不足10个则全部显示
const shortText = text.length > 10 ? text.substring(0, 10) : text;
// 添加HTML内容包括视频播放控件
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)">
<button class="btn btn-outline-primary btn-sm" onclick="toggleAudioPlayer('${item.id}', '${item.recordingPath || ''}')">
<i class="bi bi-play-fill"></i>
</button>
<button class="btn btn-outline-secondary btn-sm" onclick="downloadRecording('${item.recordingPath || ''}')">
@ -715,6 +720,12 @@
</div>
</div>
<div>${formattedDate}</div>
<div id="audio-container-${item.id}" class="audio-player-container">
<video id="audio-player-${item.id}" class="audio-player" controls>
<source src="${item.recordingPath || ''}" type="audio/wav">
您的浏览器不支持视频标签。
</video>
</div>
`;
container.appendChild(listItem);
@ -764,6 +775,7 @@
// 如果是当前正在播放的音频
if (currentAudio && currentPlayButton === button) {
currentAudio.volume = 1;
if (currentAudio.paused) {
// 如果当前是暂停状态,则继续播放
log(`继续播放音频: ${path}`);
@ -1113,6 +1125,10 @@
document.getElementById("audioStreaming1").checked = true;
isAudioStreamEnabled = true;
// 初始化UI和事件
initUI();
});
// 页面卸载前清理资源
@ -1135,9 +1151,6 @@
// 确保值在有效范围内(0或1)
if (displayType === 0 || displayType === 1) {
// 更新单选按钮状态
document.getElementById(`displayMode${displayType}`).checked = true;
log(`UI显示模式已更新为: ${displayType === 0 ? "识别立即显示" : "手动显示"}`);
}
}
@ -1172,7 +1185,7 @@
displayedText.textContent = text;
// 同时将文本填入输入框
document.getElementById("text-input").value = text;
// document.getElementById("text-input").value = text;
// 显示文本编辑区域和按钮区域
document.getElementById("input-text-area").style.display = "block";
@ -1486,243 +1499,8 @@
}
}
// 播放实时音频 - 适应新格式
function playRealTimeAudio(audioPacket) {
if (!audioContext || !isAudioStreamEnabled || !callInProgress) return;
try {
// 解析音频元数据和数据
const { format, sampleRate, channels, data } = audioPacket;
// 确保格式正确
if (!format || !data) {
log("音频格式或数据无效");
return;
}
log(`接收到音频数据: 格式=${format}, 采样率=${sampleRate}, 声道=${channels}, 数据长度=${Array.isArray(data) ? data.length : '未知'}`);
// 根据不同的音频格式处理
if (format === "WAV") {
// 处理WAV格式数据 - 异步解码
processWavData(data, sampleRate, channels)
.then(audioBuffer => {
if (audioBuffer) {
playAudioBuffer(audioBuffer);
} else {
log("无法创建WAV音频缓冲区");
}
})
.catch(err => {
log("WAV处理错误: " + err);
});
} else if (format === "16bit_PCM") {
// 处理PCM格式数据 - 同步处理
const audioBuffer = processPcmData(data, sampleRate, channels);
if (audioBuffer) {
playAudioBuffer(audioBuffer);
} else {
log("无法创建PCM音频缓冲区");
}
} else {
log("不支持的音频格式: " + format);
}
} catch (e) {
log("处理实时音频失败: " + e);
}
}
// 播放音频缓冲区
function playAudioBuffer(audioBuffer) {
// 确保音频上下文活跃
if (audioContext.state === 'suspended') {
try {
audioContext.resume();
} catch (e) {
log("恢复音频上下文失败: " + e);
return;
}
}
// 创建音频源并连接
const source = audioContext.createBufferSource();
source.buffer = audioBuffer;
// 应用音量控制
source.connect(audioGainNode);
if (audioGainNode) {
audioGainNode.gain.value = currentVolume * volumeBoost;
}
// 立即播放
source.start(0);
log("开始播放音频");
}
// 处理WAV格式的数据 - 返回Promise
function processWavData(data, sampleRate, channels) {
return new Promise((resolve, reject) => {
try {
// 确保音频上下文存在
if (!audioContext || audioContext.state === 'closed') {
initAudioContext();
if (!audioContext) {
reject(new Error("无法初始化音频上下文"));
return;
}
}
// 将数据转换为ArrayBuffer
let arrayBuffer;
if (data instanceof Uint8Array) {
arrayBuffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
} else if (Array.isArray(data)) {
// 转换数组为Uint8Array
const uint8Array = new Uint8Array(data);
arrayBuffer = uint8Array.buffer;
} else if (typeof data === 'string') {
// 处理Base64编码
try {
const base64Str = data.trim().replace(/^data:[^;]+;base64,/, '');
const binary = atob(base64Str);
const uint8Array = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
uint8Array[i] = binary.charCodeAt(i);
}
arrayBuffer = uint8Array.buffer;
} catch (e) {
log("WAV数据Base64解码失败: " + e);
reject(e);
return;
}
} else if (data.buffer) {
arrayBuffer = data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
} else {
const error = new Error("无法处理的WAV数据类型");
log(error.message);
reject(error);
return;
}
// 使用Web Audio API解码音频
audioContext.decodeAudioData(
arrayBuffer,
(buffer) => {
log("WAV数据解码成功, 时长: " + buffer.duration.toFixed(2) + "秒");
resolve(buffer);
},
(err) => {
log("解码WAV数据失败: " + err);
reject(err);
}
);
} catch (e) {
log("处理WAV数据失败: " + e);
reject(e);
}
});
}
// 处理PCM格式的数据
function processPcmData(data, sampleRate, channels) {
try {
// 确保音频上下文存在
if (!audioContext || audioContext.state === 'closed') {
initAudioContext();
if (!audioContext) return null;
}
// 转换数据为适合的格式
let pcmData;
if (data instanceof Uint8Array) {
pcmData = data;
} else if (Array.isArray(data)) {
pcmData = new Uint8Array(data);
} else if (typeof data === 'object' && data.buffer) {
pcmData = new Uint8Array(data.buffer);
} else if (typeof data === 'string') {
try {
// 处理Base64编码
const base64Str = data.trim().replace(/^data:[^;]+;base64,/, '');
const binary = atob(base64Str);
pcmData = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
pcmData[i] = binary.charCodeAt(i);
}
} catch (e) {
log("PCM数据Base64解码失败: " + e);
return null;
}
} else {
log("不支持的PCM数据类型");
return null;
}
// 确保有效的数据
if (!pcmData || pcmData.length < 2) {
log("PCM数据无效或太短");
return null;
}
// 确保数据长度是偶数16位PCM
const validLength = Math.floor(pcmData.length / 2) * 2;
if (validLength < pcmData.length) {
pcmData = pcmData.slice(0, validLength);
}
try {
// 从Uint8Array创建Int16Array视图
let int16Data;
try {
// 创建DataView以便正确解析16位整数
const dataView = new DataView(pcmData.buffer, pcmData.byteOffset, pcmData.byteLength);
int16Data = new Int16Array(pcmData.length / 2);
// 从小端字节序读取16位整数
for (let i = 0; i < pcmData.length; i += 2) {
if (i + 1 < pcmData.length) {
int16Data[i / 2] = dataView.getInt16(i, true); // true表示小端字节序
}
}
} catch (e) {
log("创建Int16Array失败: " + e);
// 备用方法
const newBuffer = new ArrayBuffer(pcmData.length);
const newView = new Uint8Array(newBuffer);
newView.set(pcmData);
const dataView = new DataView(newBuffer);
int16Data = new Int16Array(pcmData.length / 2);
for (let i = 0; i < pcmData.length; i += 2) {
if (i + 1 < pcmData.length) {
int16Data[i / 2] = dataView.getInt16(i, true);
}
}
}
// 创建音频缓冲区,使用实际采样率
const audioSampleRate = sampleRate || audioContext.sampleRate;
const buffer = audioContext.createBuffer(channels || 1, int16Data.length, audioSampleRate);
// 将Int16数据转换为Float32数据并存入缓冲区
const channelData = buffer.getChannelData(0);
for (let i = 0; i < int16Data.length; i++) {
// 将Int16转换为-1.0到1.0的Float32
channelData[i] = Math.max(-1, Math.min(1, int16Data[i] / 32768.0));
}
return buffer;
} catch (e) {
log("PCM数据处理失败: " + e);
return null;
}
} catch (e) {
log("处理PCM数据失败: " + e);
return null;
}
}
// 显示实时语音识别结果
function showRealtimeTextResult(text) {
@ -1747,9 +1525,6 @@
const displayedText = document.getElementById("displayed-text");
displayedText.innerHTML = `<span class="final-text">${text}</span>`;
// 同时将文本填入输入框,便于编辑
// document.getElementById("text-input").value = text;
log("显示最终语音识别结果");
}
@ -1852,5 +1627,230 @@
updateScreenControlUI(false);
});
}
// 初始化UI和事件
function initUI() {
// 查询当前选择的模式
if (connection && connection.state === signalR.HubConnectionState.Connected) {
// 获取当前控屏设置
getServerScreenControlSetting();
// 获取当前音频流设置
getServerAudioStreamingSetting();
// 获取当前预设句子类型设置
getServerPresetSentenceTypeSetting();
}
// 设置工具提示
var tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]'));
tooltipTriggerList.map(function (tooltipTriggerEl) {
return new bootstrap.Tooltip(tooltipTriggerEl);
});
}
// 获取服务器当前预设句子类型设置
function getServerPresetSentenceTypeSetting() {
if (!connection || connection.state !== signalR.HubConnectionState.Connected) {
log("无法获取预设句子类型设置:未连接");
return;
}
log("获取服务器当前预设句子类型设置...");
connection.invoke("GetPresetSentenceType")
.then(type => {
log(`获取到服务器当前预设句子类型设置: ${type}`);
updatePresetSentenceTypeUI(type);
})
.catch(err => {
log("获取预设句子类型设置失败: " + err);
});
}
// 更新预设句子类型UI
function updatePresetSentenceTypeUI(type) {
// 更新单选按钮状态
document.getElementById(`presetSentenceType${type}`).checked = true;
log(`UI预设句子类型设置已更新为: ${type}`);
}
// 更新预设句子类型
function updatePresetSentenceType(type) {
if (!connection || connection.state !== signalR.HubConnectionState.Connected) {
showMessage("无法更新预设句子类型:未连接到服务器", "danger");
log("更新预设句子类型失败:未连接到服务器");
return;
}
log(`正在更新预设句子类型: ${type}`);
connection.invoke("UpdatePresetSentenceType", type)
.then(result => {
if (result) {
log("预设句子类型更新成功");
showMessage(`预设句子类型已更新为: ${type === 0 ? '默认预设句子' : '预设句子2'}`, "success");
} else {
log("预设句子类型更新失败");
showMessage("预设句子类型更新失败", "danger");
}
})
.catch(err => {
log(`更新预设句子类型失败: ${err}`);
showMessage("更新预设句子类型失败", "danger");
});
}
// 添加预设句子
function addPresetSentence() {
const sentenceInput = document.getElementById("presetSentenceInput");
const text = sentenceInput.value.trim();
if (!text) {
showMessage("请输入要添加的预设句子", "warning");
log("添加预设句子失败:未输入文本");
return;
}
if (!connection || connection.state !== signalR.HubConnectionState.Connected) {
showMessage("无法添加预设句子:未连接到服务器", "danger");
log("添加预设句子失败:未连接到服务器");
return;
}
log(`正在添加预设句子: ${text}`);
connection.invoke("AddPresetSentence", text)
.then(result => {
if (result) {
log("预设句子添加成功");
showMessage("预设句子已添加", "success");
// 清空输入框
sentenceInput.value = "";
} else {
log("预设句子添加失败");
showMessage("预设句子添加失败", "danger");
}
})
.catch(err => {
log(`添加预设句子失败: ${err}`);
showMessage("添加预设句子失败", "danger");
});
}
// 添加监听器:预设句子类型已更改
function setupPresetSentenceTypeListeners() {
connection.on("PresetSentenceTypeChanged", function (type) {
log(`收到预设句子类型更改通知: ${type}`);
updatePresetSentenceTypeUI(type);
});
connection.on("PresetSentencesUpdated", function () {
log("收到预设句子更新通知");
showMessage("预设句子列表已更新", "info");
});
}
// 切换音频播放器显示/隐藏
function toggleAudioPlayer(id, path) {
if (!path) {
showMessage("无可播放的录音", "warning");
return;
}
// 获取容器和播放器元素
const container = document.getElementById(`audio-container-${id}`);
const player = document.getElementById(`audio-player-${id}`);
const button = event.currentTarget;
const buttonIcon = button.querySelector('i');
// 切换显示状态
if (container.style.display === "none" || !container.style.display) {
// 隐藏其他所有播放器
document.querySelectorAll('.audio-player-container').forEach(el => {
el.style.display = "none";
});
// 显示当前播放器
container.style.display = "block";
// 播放音频
player.play();
buttonIcon.className = "bi bi-pause-fill";
} else {
// 如果已显示,则切换播放/暂停状态
if (player.paused) {
player.play();
buttonIcon.className = "bi bi-pause-fill";
} else {
player.pause();
buttonIcon.className = "bi bi-play-fill";
}
}
// 播放结束时更新按钮图标
player.onended = function() {
buttonIcon.className = "bi bi-play-fill";
};
}
// 启动连接
async function start() {
try {
if (connection && connection.state === signalR.HubConnectionState.Connected) {
log("已经连接到Hub无需重新连接");
return;
}
debugger
if (!connection) {
log("初始化SignalR连接...");
// 创建连接
connection = new signalR.HubConnectionBuilder()
.withUrl("/audiohub")
.withAutomaticReconnect()
.build();
// 设置连接事件监听
setupConnectionEventHandlers();
// 设置消息接收处理器
setupMessageHandlers();
// 设置控屏设置变更监听器
setupScreenControlListeners();
// 设置音频流设置变更监听器
setupAudioStreamingListeners();
// 设置预设句子类型变更监听器
setupPresetSentenceTypeListeners();
}
log("正在连接到Hub...");
await connection.start();
log("成功连接到Hub");
// 注册为监控客户端
await connection.invoke("RegisterClient", "monitor");
log("已注册为监控客户端");
// 加载文本列表
loadTextLists();
// 初始化音频上下文
initAudioContext();
// 启用实时音频流
isAudioStreamEnabled = true;
// 初始化UI和事件
initUI();
} catch (err) {
log(`连接失败:${err}`);
setTimeout(start, 5000); // 5秒后重试
}
}
</script>
}

View File

@ -8,10 +8,10 @@
"fontSize": "40px",
"fontWeight": "bolder",
"fontStyle": "italic",
"typewriterSpeed": 18000,
"typewriterSpeed": 33000,
"fadeChars": 1,
"fadeStepTime": 3000,
"fadeDelayFactor": 0.20
"fadeStepTime": 5000,
"fadeDelayFactor": 0.15
},
"waterEffect": {
"enabled": true,

View File

@ -0,0 +1,62 @@
[
{
"Id": "963cc0c4-2acf-4f00-9c13-119ef85a66f1",
"Text": "就因为一条互联网的条件叠加恶意营销的影响",
"Timestamp": "2025-03-30T00:19:24.4648136+08:00",
"IsRealUser": true,
"RecognitionId": "7obT5sL_slaaK3cdqNTq-w",
"Priority": 10,
"IsProcessed": false,
"CompletedText": null,
"RecordingPath": "D:\\CodeManage\\ShengShengBuXi\\ShengShengBuXi\\ShengShengBuXi\\recordings\\recording_20250330_001916.wav",
"TextFilePath": "D:\\CodeManage\\ShengShengBuXi\\ShengShengBuXi\\ShengShengBuXi\\recordings\\recording_20250330_001916.txt"
},
{
"Id": "ea36edcc-d169-4b1f-971e-c48239c779a3",
"Text": "鸡精的AB视背景然后是盐就是加强的谷氨酸钠再加钠最后还会再加上核苷酸",
"Timestamp": "2025-03-30T00:17:12.5192552+08:00",
"IsRealUser": true,
"RecognitionId": "5QB3lEX9vbLEOgkiiy2FTw",
"Priority": 10,
"IsProcessed": false,
"CompletedText": null,
"RecordingPath": "/recordings/recording_20250330_001701.wav",
"TextFilePath": "/texts/recording_20250330_001701.txt"
},
{
"Id": "b12d7083-057f-4001-b1bc-1dacd576a4a7",
"Text": "不是,而是你的\n我期待的不是月而是和你的遇见。我期待烟花满天我可以\n永远靠在你左肩我期待\n不是一句抱歉\n失恋\n屋檐梨花\n再见",
"Timestamp": "2025-03-29T23:51:57.5244072+08:00",
"IsRealUser": true,
"RecognitionId": "RRr0moprCn6UBwu80K0orQ",
"Priority": 10,
"IsProcessed": false,
"CompletedText": null,
"RecordingPath": "/recordings/recording_20250329_235110.wav",
"TextFilePath": "/texts/recording_20250329_235110.txt"
},
{
"Id": "f6bbc349-29e5-48fe-a97c-a16dfd65cc90",
"Text": "播某濒临破产,老板儿子含泪直播,近一人观看,这家微星企业回应了,这是谣言,公司很火爆,我们现在挺好的,这是谣传,濒临破产这个事儿之前确实是真的国产味精曾经的王者某花,如何从山顶跌下悬崖,又如何起死回生呢?省油的版本是,这是一个极度典型的曾经一个农业加工类的巨无霸国产品牌,就因为一条互联网的谣言叠加恶意营销的影响,一度受挫,濒临风雨\n而他的重新复苏契机是2023年底的一支眉笔他们家在直播间顺势推出了79块钱5斤半的味精套餐呢更是因为互联网客户信息从当年妖言惑众劣币驱逐良币的时代逐渐向",
"Timestamp": "2025-03-30T00:15:32.2187442+08:00",
"IsRealUser": true,
"RecognitionId": "Eh6MPMq46zQBwUoXa9-twg",
"Priority": 10,
"IsProcessed": false,
"CompletedText": null,
"RecordingPath": "/recordings/recording_20250330_001442.wav",
"TextFilePath": "/texts/recording_20250330_001442.txt"
},
{
"Id": "ab9742d6-69fb-4754-b3aa-b47cd311520d",
"Text": "学的武和表演相衡的时代来说悬崖到当年的谣言是什么还不就是什么胃镜致癌、胃经秃头、胃经不孕这些信息在2000年左右并不是他与此同时呢以基金为代表的一些新的纤维添加剂疯狂的占据了未经失去的阵地。巧的是在1999年发生了一个事儿某家鸟类的家的公司收购了一家国产基金品牌\n是日本的饭某人有一天喝了一碗海带汤觉得很鲜就到实验室里一顿提取然后取名为未知素成分就是谷氨酸钠。拆解出来呢。谷氨酸是天然食物蛋白质里普遍存在的一种氨基酸啊但它平常是没有什么味道的比如说每100g的鸡肉的蛋白质里头就有3.6g的谷氨酸。但是你直接吃鸡肉好像并不咋鲜。你熬鸡汤的时候才泄那是谷氨酸游离出来。这个时候再加上一点儿盐于是就成了能被我们人体感知的纤维。盐的主要成分就是na嘛所以谷氨酸钠为什么先就是这个道理。那为啥说是农业加工企业呢我们看一眼味精的原料表上面儿俩字儿小麦当然几十年前还有一种玉米的反正不管是什么服务他就是在工厂里进行服务的糖化然后再进行发酵让这些天然谷物当中的谷氨酸分离出来。接下来在合适的温度和PH值的条件下和钠结合最后提纯结晶味精是有国标的而且是强制国标要求胃经里谷氨酸钠的含量要大于99%,说白了,味精为什么后面是原料表,不是配料\n因为不需要它就是提取加工出来的单一物质别的啥也没有了。可以说啊味精是所有食品添加剂当中配料最简单的。那么其他的纤维添加物呢比如说味精受挫之后最大的受益者鸡精呢鸡精的配料表排名第一是味精然后是盐就是加强的谷氨酸钠再加钠最后还会再加上核苷酸也就是再体现而多种蚝油的配料表除了水、糖、盐、蚝汁儿之外也有谷氨酸钠以及核苷酸。好我们来给结论第一味精有害是谣言但也并不意味着我们想吃多少就吃多少。谷氨酸是天然氨基酸但是值得注意的是后面那个钠中国很多地方居民饮食习惯口味\n这种食盐的摄入是容易超标的也就是钠超标。如果你又重盐又重味精或鸡精那就会加剧钠的超标。第二其他纤维添加剂当中的这个核苷酸呀吃多了容易嘌呤超标尿酸高的朋友尤其",
"Timestamp": "2025-03-30T00:10:42.3592763+08:00",
"IsRealUser": true,
"RecognitionId": "Ky5Jfjsk23S02xQY5Vbqfg",
"Priority": 10,
"IsProcessed": false,
"CompletedText": null,
"RecordingPath": "/recordings/recording_20250330_000835.wav",
"TextFilePath": "/texts/recording_20250330_000835.txt"
}
]

View File

@ -0,0 +1 @@
{"Type":1}

View File

@ -0,0 +1 @@
{"IsManual":false}

Binary file not shown.

View File

@ -0,0 +1,27 @@
记得每到夏天傍晚,您就摇着蒲扇坐在藤椅里,把切好的西瓜最甜那块硬塞给我,自己却啃着靠近皮的白瓤,还笑着说:外公就爱这口,清爽。
女儿啊 花开了 你否到了吗?
外公,你在那边过的还好么大家都很想称,记得常回家看看
外公,今天窗台上的茉莉开了,白盈盈的,就像以前您总别在中山装口袋上的那朵。
晒被子时闻到阳光味道,突然想起您总说"晒过的被子能藏住太阳"。
路过糕点铺看见桃酥,手已经伸进钱包才想起再没人等我带点心回家了。
梅雨季的墙根又生了青苔,再没人蹲着教我认哪些能入药。
公交卡掉进沙发缝的瞬间,耳边响起您笑我"马虎样像极了你奶奶年轻时"。
超市里荔枝上市了,指尖碰到冰镇外壳就想起您剥好喂我的那碗去核的。
整理旧书时飘出干枯的枫叶书签,您工整的"1973年秋于香山"已经褪色。
台风天窗户砰砰响,再没人半夜摸黑来给我房间加固防风板。
发现第一根白发时,突然懂得您当年让我拔时说的"这是月亮给的银丝"。
中药柜抽屉拉开的气味里,总错觉能听见您碾药时的咳嗽声。
女儿突然说"想太公了",我才发现她笑起来有您一样的酒窝。
煮糊的粥底粘在锅上,想起您总悄悄刮掉焦层把好的留给我。
老式挂钟敲七下时,还会下意识望向门口等那句"我买豆浆回来了"。

BIN
ShengShengBuXi/success.txt Normal file

Binary file not shown.

2042
ShengShengBuXi/temp.txt Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.