Merge branch 'feature/voice_send'
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Document</title>
|
||||
<!-- 你的其他样式 -->
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/mathjax@3/es5/tex-mml-chtml.js"></script>
|
||||
<script>
|
||||
MathJax.config = {
|
||||
tex: {
|
||||
inlineMath: [['$', '$'], ['\(', '\)']]
|
||||
},
|
||||
svg: {
|
||||
fontCache: 'global'
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
</head>
|
||||
<body>
|
||||
<h1>算法设计范式:分而治之与归并排序</h1>
|
||||
<blockquote>
|
||||
<p>Auto-generated at 2025-09-22 15:38</p>
|
||||
</blockquote>
|
||||
<h2>算法设计范式:分而治之与归并排序</h2>
|
||||
<h3>第一关:理论探究:分而治之的力量</h3>
|
||||
<p><strong>案例背景:智慧城市交通优化系统</strong></p>
|
||||
<p>在上周的工作中,你已经实现并分析了插入排序在交通数据处理中的性能。虽然它在数据量较小或基本有序的情况下表现不错,但在应对大规模拥堵(逆序数据)时,其性能瓶颈非常明显。本周,你的项目经理(Agent)将向你介绍一种更高效的算法设计思想——“分而治之”,并要求你掌握其代表算法:归并排序。</p>
|
||||
<h4>任务:理解与分析</h4>
|
||||
<h5>场景介绍</h5>
|
||||
<p>你的经理告诉你,归并排序(Merge Sort)是解决大规模排序问题的利器。在投入编码之前,你必须先从理论上理解它为何如此高效。</p>
|
||||
<h5>思考题</h5>
|
||||
<p>请与右侧的Agent(你的项目经理)对话,清晰地回答以下问题,以证明你已理解核心思想:</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p><strong>分治思想</strong>:根据课程资料,请解释什么是“分而治之”(Divide-and-Conquer)设计范式?它包含哪三个基本步骤?</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>递推关系</strong>:归并排序的时间复杂度可以用递推式 T(n)=2T(n/2)+Θ(n) 来表示。请解释这个式子各部分的含义:</p>
|
||||
<ul>
|
||||
<li><code>2</code> 代表什么?</li>
|
||||
<li><code>T(n/2)</code> 代表什么?</li>
|
||||
<li><code>Θ(n)</code> 代表什么?</li>
|
||||
</ul>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>效率对比</strong>:你的同事认为,插入排序经过极致优化后,处理n个数据的耗时为 <eq>0.1n^2</eq>;而归并排序由于递归和合并开销,耗时为 1000nlog_2n。在处理超大规模城市交通数据时(即n趋于无穷大时),哪个算法最终会胜出?请从渐进增长的角度解释你的理由。</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>增长排序</strong>:将下列函数的渐进增长率从低到高排序,并将它们划分到等价类(即 Θ 关系相同的函数)。这有助于你理解不同算法效率的所在层级。</p>
|
||||
<ul>
|
||||
<li><eq>n^2</eq> (插入排序最坏情况)</li>
|
||||
<li>nlogn (归并排序)</li>
|
||||
<li>n (一种非常低效的蛮力算法)</li>
|
||||
<li><eq>2^n</eq> (另一种指数级算法)</li>
|
||||
<li>logn (类似二分查找的效率)</li>
|
||||
</ul>
|
||||
</li>
|
||||
</ol>
|
||||
<h3>第二关:编程实践:实现归并排序</h3>
|
||||
<h4>任务:编码与对比</h4>
|
||||
<h5>场景介绍</h5>
|
||||
<p>现在你已经从理论上理解了归并排序的威力。是时候亲手实现它,并与上周的插入排序进行一次性能上的正面较量了!</p>
|
||||
<h5>题目:实现归并排序并对比性能</h5>
|
||||
<p>你需要实现归并排序的核心函数,并利用上周的测试框架来直观对比它与插入排序在处理不同交通数据时的表现。</p>
|
||||
<h5>代码框架</h5>
|
||||
<p>在下方代码编辑区,完成 <code>merge_sort(arr)</code> 和 <code>merge(left, right)</code> 两个函数的实现。</p>
|
||||
<pre><code class="language-python">import random
|
||||
import time
|
||||
|
||||
#上周实现的插入排序(用于对比)
|
||||
def insertion_sort(arr):
|
||||
for i in range(1, len(arr)):
|
||||
key = arr[i]
|
||||
j = i - 1
|
||||
while j >= 0 and arr[j] > key:
|
||||
arr[j + 1] = arr[j]
|
||||
j -= 1
|
||||
arr[j + 1] = key
|
||||
return arr
|
||||
|
||||
#--- 本周任务:请在下方实现归并排序 ---
|
||||
|
||||
def merge_sort(arr):
|
||||
"""
|
||||
实现归并排序算法
|
||||
参数arr: 待排序的交通数据数组
|
||||
返回: 一个新的、排序好的数组
|
||||
"""
|
||||
# TODO: 实现归并排序的递归逻辑
|
||||
# 提示:当数组元素个数小于等于1时,递归终止
|
||||
|
||||
|
||||
def merge(left, right):
|
||||
"""
|
||||
合并两个已排序的子数组
|
||||
参数left: 左侧已排序数组
|
||||
参数right: 右侧已排序数组
|
||||
返回: 合并后的一个有序数组
|
||||
"""
|
||||
|
||||
|
||||
#--- 测试与对比部分 ---
|
||||
|
||||
def measure_performance(sort_func, data):
|
||||
start_time = time.perf_counter_ns()
|
||||
sort_func(data.copy())
|
||||
end_time = time.perf_counter_ns()
|
||||
return (end_time - start_time) / 10**6
|
||||
|
||||
network_sizes = [1000, 5000, 10000, 50000]
|
||||
print("算法性能对比测试:")
|
||||
for size in network_sizes:
|
||||
# 生成逆序数据,这是插入排序的最坏情况
|
||||
worst_case_data = list(range(size, 0, -1))
|
||||
|
||||
time_insertion = measure_performance(insertion_sort, worst_case_data)
|
||||
time_merge = measure_performance(merge_sort, worst_case_data)
|
||||
|
||||
print(f"--- 网络规模 n={size} (最坏情况) ---")
|
||||
print(f" - 插入排序: {time_insertion:.2f} ms")
|
||||
print(f" - 归并排序: {time_merge:.2f} ms")
|
||||
</code></pre>
|
||||
<h5>分析与讨论</h5>
|
||||
<p>完成编程并得到输出后,请与右侧Agent讨论以下问题:</p>
|
||||
<ol>
|
||||
<li>
|
||||
<p><strong>性能验证</strong>:观察“交通大堵塞”(最坏情况)的测试结果,归并排序的性能表现与插入排序有何天壤之别?这是否验证了你在第一关的理论分析?</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>性能稳定性</strong>:如果我们将测试数据换成“畅通无阻”(最好情况)或“随机车流”(平均情况),你认为归并排序的运行时间会发生巨大变化吗?为什么?</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>空间成本</strong>:在实现 <code>merge</code> 函数时,你可能创建了一个新的数组 <code>merged</code> 来存放结果。这在算法分析中被称为“空间复杂度”。与在原数组上进行交换的插入排序相比,归并排序的这个特点是优点还是缺点?</p>
|
||||
</li>
|
||||
<li>
|
||||
<p><strong>最终决策</strong>:作为项目工程师,你会选择哪种排序算法用于我们的大规模交通调度系统?请综合考虑<strong>时间效率</strong>和<strong>空间成本</strong>来陈述你的理由。</p>
|
||||
</li>
|
||||
</ol>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -50,6 +50,7 @@ body {
|
||||
.dashboard {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
.dashboard-title {
|
||||
@@ -63,9 +64,13 @@ body {
|
||||
/* 课程卡片容器 */
|
||||
.course-cards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-wrap: nowrap;
|
||||
gap: 20px;
|
||||
justify-content: center;
|
||||
justify-content: flex-start;
|
||||
padding: 10px 0;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
}
|
||||
|
||||
/* 课程卡片 */
|
||||
|
||||
81
Html/apps/static/css/navbar.css
Normal file
81
Html/apps/static/css/navbar.css
Normal file
@@ -0,0 +1,81 @@
|
||||
/* 页眉导航栏 基础样式 */
|
||||
.navbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: #ffffff;
|
||||
padding: 10px 20px;
|
||||
border-bottom: 1px solid #eee;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 导航栏 Logo 样式 */
|
||||
.navbar .logo h1 {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 导航链接样式 */
|
||||
.navbar-links a {
|
||||
color: #4b5563;
|
||||
text-decoration: none;
|
||||
margin: 0 15px;
|
||||
font-size: 18px;
|
||||
transition: color 0.3s;
|
||||
}
|
||||
|
||||
.navbar-links a:hover {
|
||||
color: #2575fc;
|
||||
}
|
||||
|
||||
/* 头像容器样式 */
|
||||
.navbar .avatar {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
.navbar .avatar img {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 下拉浮框 基础定位 */
|
||||
.dropdown {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* 下拉浮框 内容样式(默认隐藏) */
|
||||
.dropdown-content {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
background-color: white;
|
||||
min-width: 160px;
|
||||
box-shadow: 0px 8px 16px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 6px;
|
||||
z-index: 1;
|
||||
padding: 8px 0;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 下拉浮框 链接样式 */
|
||||
.dropdown-content a {
|
||||
color: #4b5563;
|
||||
text-decoration: none;
|
||||
padding: 8px 12px;
|
||||
display: block;
|
||||
transition: background-color 0.3s;
|
||||
}
|
||||
|
||||
.dropdown-content a:hover {
|
||||
background-color: #f3f4f6;
|
||||
}
|
||||
|
||||
/* 下拉浮框 hover 显示 */
|
||||
.dropdown:hover .dropdown-content {
|
||||
display: block;
|
||||
}
|
||||
11
Html/apps/static/js/audio-processor.js
Normal file
11
Html/apps/static/js/audio-processor.js
Normal file
@@ -0,0 +1,11 @@
|
||||
class AudioProcessor extends AudioWorkletProcessor {
|
||||
process(inputs, outputs) {
|
||||
const input = inputs[0];
|
||||
if (input.length > 0) {
|
||||
this.port.postMessage(input[0]);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
registerProcessor('audio-processor', AudioProcessor);
|
||||
@@ -1,16 +1,16 @@
|
||||
|
||||
var socket;
|
||||
let system_message_idx=0
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
let system_message_idx = 0
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
data = window.appData;
|
||||
console.log(data);
|
||||
socket = io('wss://hsamooc.com/agent',{
|
||||
query:{
|
||||
user_uuid:data.user_uuid,
|
||||
folder:data.folder
|
||||
socket = io('wss://hsamooc.com/agent', {
|
||||
query: {
|
||||
user_uuid: data.user_uuid,
|
||||
folder: data.folder
|
||||
}
|
||||
});
|
||||
socket.on('connect', function() {
|
||||
socket.on('connect', function () {
|
||||
console.log('Connected to server');
|
||||
socket.emit('login',JSON.stringify(data));
|
||||
});
|
||||
@@ -18,10 +18,15 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
OpenIframe();
|
||||
});
|
||||
// 监听来自服务器的消息
|
||||
socket.on('message', function(data) {
|
||||
socket.on('voiceMessage', function (data) {
|
||||
console.log(data)
|
||||
const input = document.createElement('messageInput');
|
||||
input.data = data['data'];
|
||||
});
|
||||
socket.on('message', function (data) {
|
||||
// 显示服务器的回复消息
|
||||
const serverMessage = document.createElement('div');
|
||||
serverMessage.innerHTML = `<div><b>华实君</b></div><div class="chat-message server-message"> ${marked.parse(data)}</div>`;
|
||||
serverMessage.innerHTML = `<div><b>华实君:</b></div><div class="chat-message server-message"> ${marked.parse(data)}</div>`;
|
||||
document.getElementById('chatbox').appendChild(serverMessage);
|
||||
// 滚动到最新消息
|
||||
document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight;
|
||||
@@ -56,18 +61,18 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
approveButton.innerHTML = '√'; // 使用勾选符号
|
||||
approveButton.data = element;
|
||||
approveButton.className = 'approve-button';
|
||||
approveButton.onclick = function(event) {
|
||||
console.log(element);
|
||||
socket.emit('message', JSON.stringify({ type: 'function', data: element }));
|
||||
event.currentTarget.disabled = true;
|
||||
event.currentTarget.classList.add('disabled');
|
||||
approveButton.onclick = function (event) {
|
||||
console.log(element);
|
||||
socket.emit('message', JSON.stringify({ type: 'function', data: element }));
|
||||
event.currentTarget.disabled = true;
|
||||
event.currentTarget.classList.add('disabled');
|
||||
};
|
||||
requestMessage.appendChild(description);
|
||||
requestMessage.appendChild(approveButton);
|
||||
|
||||
|
||||
document.getElementById('chatbox').appendChild(requestMessage);
|
||||
document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight;
|
||||
}
|
||||
}
|
||||
// for (let i = 0; i < data.length; i++) {
|
||||
// const element = data[i]
|
||||
// const requestMessage = document.createElement('div');
|
||||
@@ -95,11 +100,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
console.log(error)
|
||||
}
|
||||
});
|
||||
socket.on('next_chapter', function(data){
|
||||
socket.on('next_chapter', function (data) {
|
||||
next_chapter(data);
|
||||
});
|
||||
socket.on('chapter_score',function(data){
|
||||
if (typeof data === 'string'){
|
||||
socket.on('chapter_score', function (data) {
|
||||
if (typeof data === 'string') {
|
||||
data = JSON.parse(data);
|
||||
}
|
||||
console.log(data)
|
||||
@@ -109,7 +114,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
document.getElementById('chatbox').appendChild(scoreMessage);
|
||||
document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight;
|
||||
})
|
||||
socket.on('system_message',function(data){
|
||||
socket.on('system_message', function (data) {
|
||||
console.log(data)
|
||||
let systemMessage = document.createElement('div');
|
||||
systemMessage.className = 'chat-message server-message';
|
||||
@@ -117,14 +122,14 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
closeButton.className = 'close-button'; // 可以根据需要添加样式类
|
||||
closeButton.innerHTML = '×'; // 关闭按钮的文本内容
|
||||
closeButton.style.marginRight = '10px'; // 可选:设置关闭按钮的右边距
|
||||
|
||||
|
||||
systemMessage.appendChild(closeButton);
|
||||
systemMessage.innerHTML += `<b>系统提示: </b> ${data}`;
|
||||
systemMessage.id = 'system_message_'+system_message_idx;
|
||||
system_message_idx+=1
|
||||
systemMessage.id = 'system_message_' + system_message_idx;
|
||||
system_message_idx += 1
|
||||
document.getElementById('chatbox').appendChild(systemMessage);
|
||||
document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight;
|
||||
$('.close-button').on('click', function(event) {
|
||||
$('.close-button').on('click', function (event) {
|
||||
$(this).css({
|
||||
'background-color': 'gray',
|
||||
'color': 'white', // 可选:设置文字颜色为白色,以便更清晰地显示
|
||||
@@ -132,7 +137,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
});
|
||||
$(this).parent().remove();
|
||||
});
|
||||
setTimeout(function() {
|
||||
setTimeout(function () {
|
||||
$(systemMessage.id).remove();
|
||||
}, 5000); // 5000 毫秒 = 5 秒
|
||||
})
|
||||
@@ -154,7 +159,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const fontSizeSlider = document.getElementById('fontSizeSlider');
|
||||
const chatbox = document.getElementById('chatbox');
|
||||
|
||||
fontSizeSlider.addEventListener('input', function() {
|
||||
fontSizeSlider.addEventListener('input', function () {
|
||||
console.log(fontSizeSlider.value);
|
||||
chatbox.style.fontSize = fontSizeSlider.value + 'px';
|
||||
});
|
||||
@@ -168,27 +173,27 @@ const chineseLabel = document.getElementById('label_for_zh');
|
||||
let language = 'zh'; // 默认语言
|
||||
|
||||
// 切换语言事件
|
||||
englishRadio.addEventListener('change', function() {
|
||||
englishRadio.addEventListener('change', function () {
|
||||
if (englishRadio.checked) {
|
||||
language = 'en';
|
||||
englishLabel.classList.add('active');
|
||||
chineseLabel.classList.remove('active');
|
||||
}
|
||||
socket.emit('language',language);
|
||||
socket.emit('language', language);
|
||||
});
|
||||
|
||||
chineseRadio.addEventListener('change', function() {
|
||||
chineseRadio.addEventListener('change', function () {
|
||||
if (chineseRadio.checked) {
|
||||
language = 'zh';
|
||||
chineseLabel.classList.add('active');
|
||||
englishLabel.classList.remove('active');
|
||||
}
|
||||
socket.emit('language',language);
|
||||
socket.emit('language', language);
|
||||
});
|
||||
|
||||
|
||||
function sendInitiativeFunctionCall(function_name){
|
||||
socket.emit('initiative', {'name': function_name});
|
||||
function sendInitiativeFunctionCall(function_name) {
|
||||
socket.emit('initiative', { 'name': function_name });
|
||||
}
|
||||
// 发送消息
|
||||
function sendMessage() {
|
||||
@@ -204,7 +209,7 @@ function sendMessage() {
|
||||
document.getElementById('chatbox').appendChild(userMessage);
|
||||
|
||||
// 发送消息到服务器
|
||||
socket.emit('message', JSON.stringify({data: input, type:'text'}));
|
||||
socket.emit('message', JSON.stringify({ data: input, type: 'text' }));
|
||||
|
||||
// 清空输入框
|
||||
document.getElementById('messageInput').value = '';
|
||||
@@ -213,6 +218,267 @@ function sendMessage() {
|
||||
document.getElementById('chatbox').scrollTop = document.getElementById('chatbox').scrollHeight;
|
||||
}
|
||||
|
||||
// let voice_stream = null;
|
||||
// let audio_chunks = [];
|
||||
// let isRecording = false;
|
||||
// let recordingInterval = null;
|
||||
// //鼠标点击终止事件
|
||||
// function handleMouseClick(event) {
|
||||
// event.stopPropagation();
|
||||
// stopRecording();
|
||||
// }
|
||||
// //发送语音
|
||||
// const startAudioRecording = async () => {
|
||||
// try {
|
||||
// const stream = await navigator.mediaDevices.getUserMedia({
|
||||
// audio: {
|
||||
// sampleRate: 16000,
|
||||
// channelCount: 1,
|
||||
// echoCancellation: true,
|
||||
// noiseSuppression: true
|
||||
// }
|
||||
// });
|
||||
// const mediaRecorder = new MediaRecorder(stream);
|
||||
// const audioContext = new AudioContext({
|
||||
// sampleRate: 16000
|
||||
// });
|
||||
|
||||
// // Load the AudioWorklet
|
||||
// await audioContext.audioWorklet.addModule('/static/js/audio-processor.js');
|
||||
|
||||
// const source = audioContext.createMediaStreamSource(stream);
|
||||
// const workletNode = new AudioWorkletNode(audioContext, 'audio-processor');
|
||||
// let audioChunks = [];
|
||||
|
||||
// workletNode.port.onmessage = (e) => {
|
||||
// audioChunks.push(new Float32Array(e.data));
|
||||
|
||||
// if (audioChunks.length >= 16) {
|
||||
// // 合并所有音频块
|
||||
// const totalLength = audioChunks.reduce((acc, chunk) => acc + chunk.length, 0);
|
||||
// const mergedData = new Float32Array(totalLength);
|
||||
// let offset = 0;
|
||||
// for (const chunk of audioChunks) {
|
||||
// mergedData.set(chunk, offset);
|
||||
// offset += chunk.length;
|
||||
// }
|
||||
|
||||
// const audioMessage = {
|
||||
// id: window.data.user_uuid,
|
||||
// type: 'audio',
|
||||
// data: mergedData,
|
||||
// sampleRate: 16000,
|
||||
// timestamp: Date.now()
|
||||
// };
|
||||
// socket.emit('voice', audioMessage);
|
||||
// console.log("voice sended")
|
||||
// audioChunks = [];
|
||||
// }
|
||||
// };
|
||||
|
||||
// source.connect(workletNode);
|
||||
// workletNode.connect(audioContext.destination);
|
||||
|
||||
// mediaRecorder.value = {
|
||||
// stop: () => {
|
||||
// source.disconnect();
|
||||
// workletNode.disconnect();
|
||||
// stream.getTracks().forEach(track => track.stop());
|
||||
// audioContext.close();
|
||||
// }
|
||||
// }
|
||||
|
||||
// isRecording.value = true;
|
||||
// } catch (error) {
|
||||
// console.error('Error starting audio recording:', error);
|
||||
// }
|
||||
// };
|
||||
// function sendVoiceMessage() {
|
||||
// startAudioRecording()
|
||||
// //变换录音中图标
|
||||
// document.getElementById('recordIcon').className = 'bi bi-record-circle';
|
||||
// document.addEventListener('click', handleMouseClick);
|
||||
// }
|
||||
|
||||
// //停止录音
|
||||
// function stopRecording() {
|
||||
// if (!isRecording) return;
|
||||
// isRecording = false;
|
||||
// //取消监听
|
||||
// document.removeEventListener('click', handleMouseClick);
|
||||
// //取消定时器
|
||||
// if (recordingInterval) {
|
||||
// clearInterval(recordingInterval);
|
||||
// recordingInterval = null;
|
||||
// }
|
||||
// //变换图标
|
||||
// document.getElementById('recordIcon').className = 'bi bi-mic';
|
||||
// //发送剩下的数据
|
||||
// if (audio_chunks.length > 0) {
|
||||
// const merged_data = new Uint8Array(audio_chunks.reduce((acc, chunk) => acc + chunk.length, 0));
|
||||
// let offset = 0;
|
||||
// for (const chunk of audio_chunks) {
|
||||
// merged_data.set(new Uint8Array(chunk), offset);
|
||||
// offset += chunk.length;
|
||||
// }
|
||||
// socket.emit('VoiceMessage', merged_data, { binary: true });
|
||||
// audio_chunks = [];
|
||||
// }
|
||||
// overlay.style.display = 'none';
|
||||
// //关闭音频流
|
||||
// if (voice_stream) {
|
||||
// voice_stream.stop_stream();
|
||||
// voice_stream.close();
|
||||
// voice_stream = null;
|
||||
// }
|
||||
// if (p) {
|
||||
// p.terminate();
|
||||
// p = null;
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
//语音这一块//
|
||||
let mediaRecorderInstance = null;
|
||||
let audioContextInstance = null;
|
||||
let workletNodeInstance = null;
|
||||
let sourceInstance = null;
|
||||
let streamInstance = null;
|
||||
let isRecording = false;
|
||||
//发送语音
|
||||
const startAudioRecording = async () => {
|
||||
try {
|
||||
const stream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
sampleRate: 16000,
|
||||
channelCount: 1,
|
||||
echoCancellation: true,
|
||||
noiseSuppression: true
|
||||
}
|
||||
});
|
||||
|
||||
// 保存实例到全局变量
|
||||
streamInstance = stream;
|
||||
const mediaRecorder = new MediaRecorder(stream);
|
||||
mediaRecorderInstance = mediaRecorder;
|
||||
|
||||
const audioContext = new AudioContext({
|
||||
sampleRate: 16000
|
||||
});
|
||||
audioContextInstance = audioContext;
|
||||
|
||||
// Load the AudioWorklet
|
||||
await audioContext.audioWorklet.addModule('/static/js/audio-processor.js');
|
||||
|
||||
const source = audioContext.createMediaStreamSource(stream);
|
||||
sourceInstance = source;
|
||||
|
||||
const workletNode = new AudioWorkletNode(audioContext, 'audio-processor');
|
||||
workletNodeInstance = workletNode;
|
||||
|
||||
audioChunks = [];
|
||||
|
||||
workletNode.port.onmessage = (e) => {
|
||||
if (!isRecording) return;
|
||||
|
||||
audioChunks.push(new Float32Array(e.data));
|
||||
|
||||
if (audioChunks.length >= 16) {
|
||||
// 合并所有音频块
|
||||
const totalLength = audioChunks.reduce((acc, chunk) => acc + chunk.length, 0);
|
||||
const mergedData = new Float32Array(totalLength);
|
||||
let offset = 0;
|
||||
for (const chunk of audioChunks) {
|
||||
mergedData.set(chunk, offset);
|
||||
offset += chunk.length;
|
||||
}
|
||||
|
||||
const audioMessage = {
|
||||
id: window.data.user_uuid,
|
||||
type: 'audio',
|
||||
data: mergedData,
|
||||
sampleRate: 16000,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
socket.emit('voice', audioMessage);
|
||||
console.log("voice sended");
|
||||
audioChunks = [];
|
||||
}
|
||||
};
|
||||
|
||||
source.connect(workletNode);
|
||||
workletNode.connect(audioContext.destination);
|
||||
|
||||
isRecording = true;
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error starting audio recording:', error);
|
||||
// 发生错误时清理资源
|
||||
stopRecording();
|
||||
}
|
||||
};
|
||||
//停止录音
|
||||
function stopRecording() {
|
||||
if (!isRecording) return;
|
||||
// 停止所有音频处理
|
||||
if (workletNodeInstance) {
|
||||
workletNodeInstance.disconnect();
|
||||
workletNodeInstance = null;
|
||||
}
|
||||
if (sourceInstance) {
|
||||
sourceInstance.disconnect();
|
||||
sourceInstance = null;
|
||||
}
|
||||
if (audioContextInstance && audioContextInstance.state !== 'closed') {
|
||||
audioContextInstance.close();
|
||||
audioContextInstance = null;
|
||||
}
|
||||
// 停止媒体流
|
||||
if (streamInstance) {
|
||||
streamInstance.getTracks().forEach(track => {
|
||||
track.stop();
|
||||
});
|
||||
streamInstance = null;
|
||||
}
|
||||
// 重置录音状态
|
||||
isRecording = false;
|
||||
mediaRecorderInstance = null;
|
||||
audioChunks = [];
|
||||
// 恢复麦克风图标
|
||||
const recordIcon = document.getElementById('recordIcon');
|
||||
if (recordIcon) {
|
||||
recordIcon.className = 'bi bi-mic';
|
||||
}
|
||||
// 移除点击事件监听器
|
||||
document.removeEventListener('click', handleMouseClick);
|
||||
}
|
||||
//鼠标点击终止事件
|
||||
function handleMouseClick(event) {
|
||||
event.stopPropagation();
|
||||
stopRecording();
|
||||
}
|
||||
function sendVoiceMessage() {
|
||||
if (isRecording) {
|
||||
stopRecording();
|
||||
return;
|
||||
}
|
||||
startAudioRecording();
|
||||
// 变换录音中图标
|
||||
const recordIcon = document.getElementById('recordIcon');
|
||||
if (recordIcon) {
|
||||
recordIcon.className = 'bi bi-record-circle';
|
||||
}
|
||||
// 添加点击事件监听器
|
||||
document.addEventListener('click', handleMouseClick);
|
||||
}
|
||||
//页面卸载时停止录音
|
||||
window.addEventListener('beforeunload', () => {
|
||||
if (isRecording) {
|
||||
stopRecording();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
let currentChapterIndex = -1;
|
||||
function next_chapter(data) {
|
||||
// 获取所有的 h3 元素
|
||||
@@ -255,22 +521,22 @@ function next_chapter(data) {
|
||||
function generateProgressBar(N, idx, titles) {
|
||||
const container = document.getElementById('markdown-content-process');
|
||||
container.innerHTML = ''; // 清空内容
|
||||
|
||||
|
||||
// 创建进度条的外框
|
||||
const progressBox = document.createElement('div');
|
||||
progressBox.className = 'progress-box';
|
||||
|
||||
|
||||
// 获取详细信息容器
|
||||
const detailBox = document.getElementById('progress-detail');
|
||||
|
||||
|
||||
// 根据N值动态调整每个进度节点的宽度
|
||||
const nodeWidth = 100 / N; // 每个节点的宽度为 100% / N
|
||||
|
||||
|
||||
// 根据N值生成子框并设置颜色
|
||||
for (let i = 0; i < N; i++) {
|
||||
const titleBox = document.createElement('div');
|
||||
titleBox.className = 'progress-title';
|
||||
|
||||
|
||||
// 设置进度条的颜色
|
||||
if (i < idx) {
|
||||
titleBox.classList.add('green'); // 已完成部分
|
||||
@@ -285,7 +551,7 @@ function generateProgressBar(N, idx, titles) {
|
||||
titleBox.style.width = `${nodeWidth}%`;
|
||||
|
||||
// 监听鼠标移入事件来显示详情
|
||||
titleBox.addEventListener('mouseenter', function() {
|
||||
titleBox.addEventListener('mouseenter', function () {
|
||||
detailBox.innerHTML = `Step ${i + 1}: ${titles[i] || "No title"}`;// - Additional details here...`;
|
||||
detailBox.style.display = 'block';
|
||||
// 根据进度条位置显示详情
|
||||
@@ -295,7 +561,7 @@ function generateProgressBar(N, idx, titles) {
|
||||
});
|
||||
|
||||
// 监听鼠标移出事件来隐藏详情
|
||||
titleBox.addEventListener('mouseleave', function() {
|
||||
titleBox.addEventListener('mouseleave', function () {
|
||||
detailBox.style.display = 'none';
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user