312321
This commit is contained in:
parent
c626311dbf
commit
9f9381de9a
|
|
@ -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)
|
||||
{
|
||||
// 将两个字节转换为short(16位)
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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("录音流程被用户取消");
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"SignalRHubUrl": "http://115.159.44.16/audiohub",
|
||||
"SignalRHubUrl": "http://localhost:81/audiohub",
|
||||
"ConfigBackupPath": "config.json",
|
||||
"AutoConnectToServer": true,
|
||||
"AllowOfflineStart": false
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
2042
ShengShengBuXi/Monitor.cshtml.bak
Normal file
2042
ShengShengBuXi/Monitor.cshtml.bak
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -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">
|
||||
|
|
@ -456,7 +529,7 @@
|
|||
</div>
|
||||
|
||||
@section Scripts {
|
||||
@* <script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/6.0.6/signalr.min.js"></script> *@
|
||||
@* <script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/6.0.6/signalr.min.js"></script> *@
|
||||
<script src="~/lib/microsoft-signalr/signalr.min.js"></script>
|
||||
|
||||
<script>
|
||||
|
|
@ -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>
|
||||
|
|
@ -533,36 +607,39 @@
|
|||
// setConnectionStatusHandlers();
|
||||
|
||||
// 设置消息处理
|
||||
connection.on("ReceiveServerMessage", function(message) {
|
||||
connection.on("ReceiveServerMessage", function (message) {
|
||||
log("服务器消息: " + message);
|
||||
showMessage(message);
|
||||
});
|
||||
|
||||
// 启动连接
|
||||
connection.start()
|
||||
.then(function() {
|
||||
.then(function () {
|
||||
log("SignalR连接成功");
|
||||
updateConnectionStatus("已连接", "success");
|
||||
|
||||
// 设置SignalR事件处理器
|
||||
setupSignalRHandlers();
|
||||
|
||||
// 注册为管理员客户端
|
||||
connection.invoke("RegisterClient", 1, "WebAdmin")
|
||||
.then(function() {
|
||||
.then(function () {
|
||||
log("注册为管理员客户端成功");
|
||||
// 获取最新配置
|
||||
getLatestConfig();
|
||||
// 获取显示配置
|
||||
getDisplayConfig();
|
||||
// 获取客户端列表
|
||||
//getClientList();
|
||||
getClientList();
|
||||
setTimeout(getRecentRecordings, 1000);
|
||||
setTimeout(getRealUserRecords, 1500);
|
||||
})
|
||||
.catch(function(err) {
|
||||
.catch(function (err) {
|
||||
log("注册为管理员客户端失败: " + err);
|
||||
showMessage("注册为管理员客户端失败,请刷新页面重试", "danger");
|
||||
});
|
||||
})
|
||||
.catch(function(err) {
|
||||
.catch(function (err) {
|
||||
log("SignalR连接失败: " + err);
|
||||
updateConnectionStatus("连接失败", "danger");
|
||||
setTimeout(initSignalR, 5000); // 5秒后重试
|
||||
|
|
@ -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>
|
||||
`;
|
||||
|
|
@ -1370,7 +1489,7 @@
|
|||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('HEAD', `/recordings/${fileName}`, true);
|
||||
|
||||
xhr.onreadystatechange = function() {
|
||||
xhr.onreadystatechange = function () {
|
||||
if (xhr.readyState === 4) {
|
||||
if (xhr.status === 200) {
|
||||
// 获取Content-Length头
|
||||
|
|
@ -1602,13 +1721,13 @@
|
|||
}
|
||||
|
||||
// 页面加载完成后初始化
|
||||
document.addEventListener("DOMContentLoaded", function() {
|
||||
document.addEventListener("DOMContentLoaded", function () {
|
||||
log("页面已加载");
|
||||
// 初始化SignalR连接
|
||||
initSignalR();
|
||||
|
||||
// 切换到录音标签页时自动刷新录音列表
|
||||
document.getElementById('recordings-tab').addEventListener('click', function() {
|
||||
document.getElementById('recordings-tab').addEventListener('click', function () {
|
||||
if (connection && connection.state === signalR.HubConnectionState.Connected) {
|
||||
log("切换到录音标签页,自动刷新录音列表");
|
||||
setTimeout(getRecentRecordings, 500);
|
||||
|
|
@ -1616,7 +1735,7 @@
|
|||
});
|
||||
|
||||
// 切换到显示配置标签页时自动加载显示配置
|
||||
document.getElementById('display-config-tab').addEventListener('click', function() {
|
||||
document.getElementById('display-config-tab').addEventListener('click', function () {
|
||||
if (connection && connection.state === signalR.HubConnectionState.Connected) {
|
||||
log("切换到显示配置标签页,自动加载显示配置");
|
||||
setTimeout(getDisplayConfig, 500);
|
||||
|
|
@ -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>
|
||||
}
|
||||
|
|
@ -316,7 +316,7 @@
|
|||
// 整条文字渐显效果
|
||||
newP.animate(
|
||||
{ opacity: 1 },
|
||||
2000, // 设置动画时长为2秒
|
||||
3000, // 设置动画时长为2秒
|
||||
'swing'
|
||||
);
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -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,36 +241,7 @@
|
|||
</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 {
|
||||
@section Scripts {
|
||||
<script src="~/lib/microsoft-signalr/signalr.min.js"></script>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.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);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
// 初始化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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
62
ShengShengBuXi/config/monitor_text_queue.json
Normal file
62
ShengShengBuXi/config/monitor_text_queue.json
Normal 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"
|
||||
}
|
||||
]
|
||||
1
ShengShengBuXi/config/newconfig.js
Normal file
1
ShengShengBuXi/config/newconfig.js
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"Type":1}
|
||||
1
ShengShengBuXi/config/screen_control.json
Normal file
1
ShengShengBuXi/config/screen_control.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"IsManual":false}
|
||||
Binary file not shown.
27
ShengShengBuXi/config/sentences2.txt
Normal file
27
ShengShengBuXi/config/sentences2.txt
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
记得每到夏天傍晚,您就摇着蒲扇坐在藤椅里,把切好的西瓜最甜那块硬塞给我,自己却啃着靠近皮的白瓤,还笑着说:外公就爱这口,清爽。
|
||||
女儿啊 花开了 你否到了吗?
|
||||
外公,你在那边过的还好么大家都很想称,记得常回家看看
|
||||
外公,今天窗台上的茉莉开了,白盈盈的,就像以前您总别在中山装口袋上的那朵。
|
||||
晒被子时闻到阳光味道,突然想起您总说"晒过的被子能藏住太阳"。
|
||||
|
||||
路过糕点铺看见桃酥,手已经伸进钱包才想起再没人等我带点心回家了。
|
||||
|
||||
梅雨季的墙根又生了青苔,再没人蹲着教我认哪些能入药。
|
||||
|
||||
公交卡掉进沙发缝的瞬间,耳边响起您笑我"马虎样像极了你奶奶年轻时"。
|
||||
|
||||
超市里荔枝上市了,指尖碰到冰镇外壳就想起您剥好喂我的那碗去核的。
|
||||
|
||||
整理旧书时飘出干枯的枫叶书签,您工整的"1973年秋于香山"已经褪色。
|
||||
|
||||
台风天窗户砰砰响,再没人半夜摸黑来给我房间加固防风板。
|
||||
|
||||
发现第一根白发时,突然懂得您当年让我拔时说的"这是月亮给的银丝"。
|
||||
|
||||
中药柜抽屉拉开的气味里,总错觉能听见您碾药时的咳嗽声。
|
||||
|
||||
女儿突然说"想太公了",我才发现她笑起来有您一样的酒窝。
|
||||
|
||||
煮糊的粥底粘在锅上,想起您总悄悄刮掉焦层把好的留给我。
|
||||
|
||||
老式挂钟敲七下时,还会下意识望向门口等那句"我买豆浆回来了"。
|
||||
BIN
ShengShengBuXi/success.txt
Normal file
BIN
ShengShengBuXi/success.txt
Normal file
Binary file not shown.
2042
ShengShengBuXi/temp.txt
Normal file
2042
ShengShengBuXi/temp.txt
Normal file
File diff suppressed because it is too large
Load Diff
1981
ShengShengBuXi/wwwroot/css/bootstrap-icons.css
vendored
Normal file
1981
ShengShengBuXi/wwwroot/css/bootstrap-icons.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
BIN
ShengShengBuXi/wwwroot/css/fonts/bootstrap-icons.woff
Normal file
BIN
ShengShengBuXi/wwwroot/css/fonts/bootstrap-icons.woff
Normal file
Binary file not shown.
BIN
ShengShengBuXi/wwwroot/css/fonts/bootstrap-icons.woff2
Normal file
BIN
ShengShengBuXi/wwwroot/css/fonts/bootstrap-icons.woff2
Normal file
Binary file not shown.
Loading…
Reference in New Issue
Block a user