diff --git a/dsLightRag/Config/__pycache__/Config.cpython-310.pyc b/dsLightRag/Config/__pycache__/Config.cpython-310.pyc index f982f569..ea70d283 100644 Binary files a/dsLightRag/Config/__pycache__/Config.cpython-310.pyc and b/dsLightRag/Config/__pycache__/Config.cpython-310.pyc differ diff --git a/dsLightRag/Routes/XueBanRoute.py b/dsLightRag/Routes/XueBanRoute.py index bdacd486..23b41983 100644 --- a/dsLightRag/Routes/XueBanRoute.py +++ b/dsLightRag/Routes/XueBanRoute.py @@ -4,8 +4,11 @@ import tempfile import uuid from datetime import datetime -from fastapi import APIRouter, Request, File, UploadFile -from fastapi.responses import JSONResponse +from fastapi import APIRouter, WebSocket, WebSocketDisconnect + +from Util.ASRClient import ASRClient +from Util.ObsUtil import ObsUploader +from Util.XueBanUtil import get_xueban_response_async, stream_and_split_text, StreamingVolcanoTTS # 创建路由路由器 router = APIRouter(prefix="/api", tags=["学伴"]) @@ -13,84 +16,125 @@ router = APIRouter(prefix="/api", tags=["学伴"]) # 配置日志 logger = logging.getLogger(__name__) -# 导入学伴工具函数、ASR客户端和OBS上传工具 -from Util.XueBanUtil import get_xueban_response_async -from Util.ASRClient import ASRClient -from Util.ObsUtil import ObsUploader -# 新增导入TTSService -from Util.TTSService import TTSService - - -@router.post("/xueban/upload-audio") -async def upload_audio(file: UploadFile = File(...)): - """ - 上传音频文件并进行ASR处理 - - 参数: file - 音频文件 - - 返回: JSON包含识别结果 - """ +# 新增WebSocket接口,用于流式处理 +@router.websocket("/xueban/streaming-chat") +async def streaming_chat(websocket: WebSocket): + await websocket.accept() + logger.info("WebSocket连接已接受") try: - # 记录日志 - logger.info(f"接收到音频文件: {file.filename}") - - # 保存临时文件 + # 接收用户音频文件 + logger.info("等待接收音频数据...") + data = await websocket.receive_json() + logger.info(f"接收到数据类型: {type(data)}") + logger.info(f"接收到数据内容: {data.keys() if isinstance(data, dict) else '非字典类型'}") + + # 检查数据格式 + if not isinstance(data, dict): + logger.error(f"接收到的数据不是字典类型,而是: {type(data)}") + await websocket.send_json({"type": "error", "message": "数据格式错误"}) + return + + audio_data = data.get("audio_data") + logger.info(f"音频数据是否存在: {audio_data is not None}") + logger.info(f"音频数据长度: {len(audio_data) if audio_data else 0}") + + if not audio_data: + logger.error("未收到音频数据") + await websocket.send_json({"type": "error", "message": "未收到音频数据"}) + return + + # 保存临时音频文件 timestamp = datetime.now().strftime("%Y%m%d%H%M%S") - file_ext = os.path.splitext(file.filename)[1] - temp_file_name = f"temp_audio_{timestamp}{file_ext}" - temp_file_path = os.path.join(tempfile.gettempdir(), temp_file_name) - - with open(temp_file_path, "wb") as f: - content = await file.read() - f.write(content) - - logger.info(f"音频文件已保存至临时目录: {temp_file_path}") - - # 调用ASR服务进行处理 - asr_result = await process_asr(temp_file_path) - - # 删除临时文件 - os.remove(temp_file_path) - logger.info(f"临时文件已删除: {temp_file_path}") - - # 使用大模型生成反馈 - logger.info(f"使用大模型生成反馈,输入文本: {asr_result['text']}") - response_generator = get_xueban_response_async(asr_result['text'], stream=False) - feedback_text = "" - async for chunk in response_generator: - feedback_text += chunk - logger.info(f"大模型反馈生成完成: {feedback_text}") - - # 使用TTS生成语音 - tts_service = TTSService() - tts_temp_file = os.path.join(tempfile.gettempdir(), f"tts_{timestamp}.mp3") - success = tts_service.synthesize(feedback_text, output_file=tts_temp_file) - if not success: - raise Exception("TTS语音合成失败") - logger.info(f"TTS语音合成成功,文件保存至: {tts_temp_file}") - - # 上传TTS音频文件到OBS - tts_audio_url = upload_file_to_obs(tts_temp_file) - os.remove(tts_temp_file) # 删除临时TTS文件 - logger.info(f"TTS文件已上传至OBS: {tts_audio_url}") - - # 返回结果,包含ASR文本和TTS音频URL - return JSONResponse(content={ - "success": True, - "message": "音频处理和语音反馈生成成功", - "data": { - "asr_text": asr_result['text'], - "feedback_text": feedback_text, - "audio_url": tts_audio_url - } - }) + temp_file_path = os.path.join(tempfile.gettempdir(), f"temp_audio_{timestamp}.wav") + logger.info(f"保存临时音频文件到: {temp_file_path}") + + # 解码base64音频数据并保存 + import base64 + try: + with open(temp_file_path, "wb") as f: + f.write(base64.b64decode(audio_data)) + logger.info("音频文件保存完成") + except Exception as e: + logger.error(f"音频文件保存失败: {str(e)}") + await websocket.send_json({"type": "error", "message": f"音频文件保存失败: {str(e)}"}) + return + + # 处理ASR + logger.info("开始ASR处理...") + try: + asr_result = await process_asr(temp_file_path) + logger.info(f"ASR处理完成,结果: {asr_result['text']}") + os.remove(temp_file_path) # 删除临时文件 + except Exception as e: + logger.error(f"ASR处理失败: {str(e)}") + await websocket.send_json({"type": "error", "message": f"ASR处理失败: {str(e)}"}) + if os.path.exists(temp_file_path): + os.remove(temp_file_path) # 确保删除临时文件 + return + + # 发送ASR结果给前端 + logger.info("发送ASR结果给前端") + try: + await websocket.send_json({ + "type": "asr_result", + "text": asr_result['text'] + }) + logger.info("ASR结果发送成功") + except Exception as e: + logger.error(f"发送ASR结果失败: {str(e)}") + return + # 定义音频回调函数,将音频块发送给前端 + async def audio_callback(audio_chunk): + logger.info(f"发送音频块,大小: {len(audio_chunk)}") + try: + await websocket.send_bytes(audio_chunk) + logger.info("音频块发送成功") + except Exception as e: + logger.error(f"发送音频块失败: {str(e)}") + raise + + # 实时获取LLM流式输出并处理 + logger.info("开始LLM流式处理和TTS合成...") + try: + # 获取LLM流式响应 + llm_stream = get_xueban_response_async(asr_result['text'], stream=True) + + # 使用stream_and_split_text处理流式响应并断句 + text_stream = stream_and_split_text(llm_stream=llm_stream) + + # 初始化TTS处理器 + tts = StreamingVolcanoTTS(max_concurrency=1) + + # 异步迭代文本流,按句合成TTS + async for text_chunk in text_stream: + logger.info(f"正在处理句子: {text_chunk}") + await tts._synthesize_single_with_semaphore(text_chunk, audio_callback) + logger.info("TTS合成完成") + except Exception as e: + logger.error(f"TTS合成失败: {str(e)}") + await websocket.send_json({"type": "error", "message": f"TTS合成失败: {str(e)}"}) + return + + # 发送结束信号 + logger.info("发送结束信号") + try: + await websocket.send_json({"type": "end"}) + logger.info("结束信号发送成功") + except Exception as e: + logger.error(f"发送结束信号失败: {str(e)}") + return + + except WebSocketDisconnect: + logger.info("客户端断开连接") except Exception as e: - logger.error(f"音频处理失败: {str(e)}") - return JSONResponse(content={ - "success": False, - "message": f"音频处理失败: {str(e)}" - }, status_code=500) - + logger.error(f"WebSocket处理失败: {str(e)}") + try: + await websocket.send_json({"type": "error", "message": str(e)}) + except: + logger.error("发送错误消息失败") +# 原有的辅助函数保持不变 async def process_asr(audio_path: str) -> dict: """ 调用ASR服务处理音频文件 @@ -100,16 +144,12 @@ async def process_asr(audio_path: str) -> dict: try: # 上传文件到华为云OBS audio_url = upload_file_to_obs(audio_path) - # 创建ASR客户端实例 asr_client = ASRClient() - # 设置音频文件URL asr_client.file_url = audio_url - # 处理ASR任务并获取文本结果 text_result = asr_client.process_task() - # 构建返回结果 return { "text": text_result, @@ -157,49 +197,4 @@ def upload_file_to_obs(file_path: str) -> str: raise Exception(error_msg) except Exception as e: logger.error(f"上传文件到OBS失败: {str(e)}") - raise - - -@router.post("/xueban/chat") -async def chat_with_xueban(request: Request): - """ - 与学伴大模型聊天的接口 - - 参数: request body 中的 query_text (用户查询文本) - - 返回: JSON包含聊天响应 - """ - try: - # 获取请求体数据 - data = await request.json() - query_text = data.get("query_text", "") - - if not query_text.strip(): - return JSONResponse(content={ - "success": False, - "message": "查询文本不能为空" - }, status_code=400) - - # 记录日志 - logger.info(f"接收到学伴聊天请求: {query_text}") - - # 调用异步接口获取学伴响应 - response_content = [] - async for chunk in get_xueban_response_async(query_text, stream=True): - response_content.append(chunk) - - full_response = "".join(response_content) - - # 返回响应 - return JSONResponse(content={ - "success": True, - "message": "聊天成功", - "data": { - "response": full_response - } - }) - - except Exception as e: - logger.error(f"学伴聊天失败: {str(e)}") - return JSONResponse(content={ - "success": False, - "message": f"聊天处理失败: {str(e)}" - }, status_code=500) \ No newline at end of file + raise \ No newline at end of file diff --git a/dsLightRag/Routes/__pycache__/XueBanRoute.cpython-310.pyc b/dsLightRag/Routes/__pycache__/XueBanRoute.cpython-310.pyc index d7fe8fcf..1d83a17f 100644 Binary files a/dsLightRag/Routes/__pycache__/XueBanRoute.cpython-310.pyc and b/dsLightRag/Routes/__pycache__/XueBanRoute.cpython-310.pyc differ diff --git a/dsLightRag/Start.py b/dsLightRag/Start.py index 96936c5e..183caef3 100644 --- a/dsLightRag/Start.py +++ b/dsLightRag/Start.py @@ -2,6 +2,7 @@ import uvicorn import asyncio from fastapi import FastAPI from starlette.staticfiles import StaticFiles +from fastapi.middleware.cors import CORSMiddleware # 添加此导入 from Routes.TeachingModel.tasks.BackgroundTasks import train_document_task from Util.PostgreSQLUtil import init_postgres_pool, close_postgres_pool @@ -26,6 +27,7 @@ from Routes.MjRoute import router as mj_router from Routes.QWenImageRoute import router as qwen_image_router from Util.LightRagUtil import * from contextlib import asynccontextmanager +import logging # 添加此导入 # 控制日志输出 logger = logging.getLogger('lightrag') @@ -37,8 +39,8 @@ logger.addHandler(handler) @asynccontextmanager async def lifespan(_: FastAPI): - pool = await init_postgres_pool() - app.state.pool = pool + #pool = await init_postgres_pool() + #app.state.pool = pool asyncio.create_task(train_document_task()) @@ -46,12 +48,21 @@ async def lifespan(_: FastAPI): yield finally: # 应用关闭时销毁连接池 - await close_postgres_pool(pool) + #await close_postgres_pool(pool) pass app = FastAPI(lifespan=lifespan) +# 添加CORS中间件 +app.add_middleware( + CORSMiddleware, + allow_origins=["*"], # 允许所有来源,生产环境中可以限制为特定域名 + allow_credentials=True, + allow_methods=["*"], # 允许所有方法 + allow_headers=["*"], # 允许所有头部 +) + # 挂载静态文件目录 app.mount("/static", StaticFiles(directory="Static"), name="static") diff --git a/dsLightRag/Util/TTS_Protocols.py b/dsLightRag/Util/TTS_Protocols.py new file mode 100644 index 00000000..6d76488a --- /dev/null +++ b/dsLightRag/Util/TTS_Protocols.py @@ -0,0 +1,543 @@ +import io +import logging +import struct +from dataclasses import dataclass +from enum import IntEnum +from typing import Callable, List + +import websockets + +logger = logging.getLogger(__name__) + + +class MsgType(IntEnum): + """Message type enumeration""" + + Invalid = 0 + FullClientRequest = 0b1 + AudioOnlyClient = 0b10 + FullServerResponse = 0b1001 + AudioOnlyServer = 0b1011 + FrontEndResultServer = 0b1100 + Error = 0b1111 + + # Alias + ServerACK = AudioOnlyServer + + def __str__(self) -> str: + return self.name if self.name else f"MsgType({self.value})" + + +class MsgTypeFlagBits(IntEnum): + """Message type flag bits""" + + NoSeq = 0 # Non-terminal packet with no sequence + PositiveSeq = 0b1 # Non-terminal packet with sequence > 0 + LastNoSeq = 0b10 # Last packet with no sequence + NegativeSeq = 0b11 # Last packet with sequence < 0 + WithEvent = 0b100 # Payload contains event number (int32) + + +class VersionBits(IntEnum): + """Version bits""" + + Version1 = 1 + Version2 = 2 + Version3 = 3 + Version4 = 4 + + +class HeaderSizeBits(IntEnum): + """Header size bits""" + + HeaderSize4 = 1 + HeaderSize8 = 2 + HeaderSize12 = 3 + HeaderSize16 = 4 + + +class SerializationBits(IntEnum): + """Serialization method bits""" + + Raw = 0 + JSON = 0b1 + Thrift = 0b11 + Custom = 0b1111 + + +class CompressionBits(IntEnum): + """Compression method bits""" + + None_ = 0 + Gzip = 0b1 + Custom = 0b1111 + + +class EventType(IntEnum): + """Event type enumeration""" + + None_ = 0 # Default event + + # 1 ~ 49 Upstream Connection events + StartConnection = 1 + StartTask = 1 # Alias of StartConnection + FinishConnection = 2 + FinishTask = 2 # Alias of FinishConnection + + # 50 ~ 99 Downstream Connection events + ConnectionStarted = 50 # Connection established successfully + TaskStarted = 50 # Alias of ConnectionStarted + ConnectionFailed = 51 # Connection failed (possibly due to authentication failure) + TaskFailed = 51 # Alias of ConnectionFailed + ConnectionFinished = 52 # Connection ended + TaskFinished = 52 # Alias of ConnectionFinished + + # 100 ~ 149 Upstream Session events + StartSession = 100 + CancelSession = 101 + FinishSession = 102 + + # 150 ~ 199 Downstream Session events + SessionStarted = 150 + SessionCanceled = 151 + SessionFinished = 152 + SessionFailed = 153 + UsageResponse = 154 # Usage response + ChargeData = 154 # Alias of UsageResponse + + # 200 ~ 249 Upstream general events + TaskRequest = 200 + UpdateConfig = 201 + + # 250 ~ 299 Downstream general events + AudioMuted = 250 + + # 300 ~ 349 Upstream TTS events + SayHello = 300 + + # 350 ~ 399 Downstream TTS events + TTSSentenceStart = 350 + TTSSentenceEnd = 351 + TTSResponse = 352 + TTSEnded = 359 + PodcastRoundStart = 360 + PodcastRoundResponse = 361 + PodcastRoundEnd = 362 + + # 450 ~ 499 Downstream ASR events + ASRInfo = 450 + ASRResponse = 451 + ASREnded = 459 + + # 500 ~ 549 Upstream dialogue events + ChatTTSText = 500 # (Ground-Truth-Alignment) text for speech synthesis + + # 550 ~ 599 Downstream dialogue events + ChatResponse = 550 + ChatEnded = 559 + + # 650 ~ 699 Downstream dialogue events + # Events for source (original) language subtitle + SourceSubtitleStart = 650 + SourceSubtitleResponse = 651 + SourceSubtitleEnd = 652 + # Events for target (translation) language subtitle + TranslationSubtitleStart = 653 + TranslationSubtitleResponse = 654 + TranslationSubtitleEnd = 655 + + def __str__(self) -> str: + return self.name if self.name else f"EventType({self.value})" + + +@dataclass +class Message: + """Message object + + Message format: + 0 1 2 3 + | 0 1 2 3 4 5 6 7 | 0 1 2 3 4 5 6 7 | 0 1 2 3 4 5 6 7 | 0 1 2 3 4 5 6 7 | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Version | Header Size | Msg Type | Flags | + | (4 bits) | (4 bits) | (4 bits) | (4 bits) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | Serialization | Compression | Reserved | + | (4 bits) | (4 bits) | (8 bits) | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + | Optional Header Extensions | + | (if Header Size > 1) | + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | | + | Payload | + | (variable length) | + | | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + """ + + version: VersionBits = VersionBits.Version1 + header_size: HeaderSizeBits = HeaderSizeBits.HeaderSize4 + type: MsgType = MsgType.Invalid + flag: MsgTypeFlagBits = MsgTypeFlagBits.NoSeq + serialization: SerializationBits = SerializationBits.JSON + compression: CompressionBits = CompressionBits.None_ + + event: EventType = EventType.None_ + session_id: str = "" + connect_id: str = "" + sequence: int = 0 + error_code: int = 0 + + payload: bytes = b"" + + @classmethod + def from_bytes(cls, data: bytes) -> "Message": + """Create message object from bytes""" + if len(data) < 3: + raise ValueError( + f"Data too short: expected at least 3 bytes, got {len(data)}" + ) + + type_and_flag = data[1] + msg_type = MsgType(type_and_flag >> 4) + flag = MsgTypeFlagBits(type_and_flag & 0b00001111) + + msg = cls(type=msg_type, flag=flag) + msg.unmarshal(data) + return msg + + def marshal(self) -> bytes: + """Serialize message to bytes""" + buffer = io.BytesIO() + + # Write header + header = [ + (self.version << 4) | self.header_size, + (self.type << 4) | self.flag, + (self.serialization << 4) | self.compression, + ] + + header_size = 4 * self.header_size + if padding := header_size - len(header): + header.extend([0] * padding) + + buffer.write(bytes(header)) + + # Write other fields + writers = self._get_writers() + for writer in writers: + writer(buffer) + + return buffer.getvalue() + + def unmarshal(self, data: bytes) -> None: + """Deserialize message from bytes""" + buffer = io.BytesIO(data) + + # Read version and header size + version_and_header_size = buffer.read(1)[0] + self.version = VersionBits(version_and_header_size >> 4) + self.header_size = HeaderSizeBits(version_and_header_size & 0b00001111) + + # Skip second byte + buffer.read(1) + + # Read serialization and compression methods + serialization_compression = buffer.read(1)[0] + self.serialization = SerializationBits(serialization_compression >> 4) + self.compression = CompressionBits(serialization_compression & 0b00001111) + + # Skip header padding + header_size = 4 * self.header_size + read_size = 3 + if padding_size := header_size - read_size: + buffer.read(padding_size) + + # Read other fields + readers = self._get_readers() + for reader in readers: + reader(buffer) + + # Check for remaining data + remaining = buffer.read() + if remaining: + raise ValueError(f"Unexpected data after message: {remaining}") + + def _get_writers(self) -> List[Callable[[io.BytesIO], None]]: + """Get list of writer functions""" + writers = [] + + if self.flag == MsgTypeFlagBits.WithEvent: + writers.extend([self._write_event, self._write_session_id]) + + if self.type in [ + MsgType.FullClientRequest, + MsgType.FullServerResponse, + MsgType.FrontEndResultServer, + MsgType.AudioOnlyClient, + MsgType.AudioOnlyServer, + ]: + if self.flag in [MsgTypeFlagBits.PositiveSeq, MsgTypeFlagBits.NegativeSeq]: + writers.append(self._write_sequence) + elif self.type == MsgType.Error: + writers.append(self._write_error_code) + else: + raise ValueError(f"Unsupported message type: {self.type}") + + writers.append(self._write_payload) + return writers + + def _get_readers(self) -> List[Callable[[io.BytesIO], None]]: + """Get list of reader functions""" + readers = [] + + if self.type in [ + MsgType.FullClientRequest, + MsgType.FullServerResponse, + MsgType.FrontEndResultServer, + MsgType.AudioOnlyClient, + MsgType.AudioOnlyServer, + ]: + if self.flag in [MsgTypeFlagBits.PositiveSeq, MsgTypeFlagBits.NegativeSeq]: + readers.append(self._read_sequence) + elif self.type == MsgType.Error: + readers.append(self._read_error_code) + else: + raise ValueError(f"Unsupported message type: {self.type}") + + if self.flag == MsgTypeFlagBits.WithEvent: + readers.extend( + [self._read_event, self._read_session_id, self._read_connect_id] + ) + + readers.append(self._read_payload) + return readers + + def _write_event(self, buffer: io.BytesIO) -> None: + """Write event""" + buffer.write(struct.pack(">i", self.event)) + + def _write_session_id(self, buffer: io.BytesIO) -> None: + """Write session ID""" + if self.event in [ + EventType.StartConnection, + EventType.FinishConnection, + EventType.ConnectionStarted, + EventType.ConnectionFailed, + ]: + return + + session_id_bytes = self.session_id.encode("utf-8") + size = len(session_id_bytes) + if size > 0xFFFFFFFF: + raise ValueError(f"Session ID size ({size}) exceeds max(uint32)") + + buffer.write(struct.pack(">I", size)) + if size > 0: + buffer.write(session_id_bytes) + + def _write_sequence(self, buffer: io.BytesIO) -> None: + """Write sequence number""" + buffer.write(struct.pack(">i", self.sequence)) + + def _write_error_code(self, buffer: io.BytesIO) -> None: + """Write error code""" + buffer.write(struct.pack(">I", self.error_code)) + + def _write_payload(self, buffer: io.BytesIO) -> None: + """Write payload""" + size = len(self.payload) + if size > 0xFFFFFFFF: + raise ValueError(f"Payload size ({size}) exceeds max(uint32)") + + buffer.write(struct.pack(">I", size)) + buffer.write(self.payload) + + def _read_event(self, buffer: io.BytesIO) -> None: + """Read event""" + event_bytes = buffer.read(4) + if event_bytes: + self.event = EventType(struct.unpack(">i", event_bytes)[0]) + + def _read_session_id(self, buffer: io.BytesIO) -> None: + """Read session ID""" + if self.event in [ + EventType.StartConnection, + EventType.FinishConnection, + EventType.ConnectionStarted, + EventType.ConnectionFailed, + EventType.ConnectionFinished, + ]: + return + + size_bytes = buffer.read(4) + if size_bytes: + size = struct.unpack(">I", size_bytes)[0] + if size > 0: + session_id_bytes = buffer.read(size) + if len(session_id_bytes) == size: + self.session_id = session_id_bytes.decode("utf-8") + + def _read_connect_id(self, buffer: io.BytesIO) -> None: + """Read connection ID""" + if self.event in [ + EventType.ConnectionStarted, + EventType.ConnectionFailed, + EventType.ConnectionFinished, + ]: + size_bytes = buffer.read(4) + if size_bytes: + size = struct.unpack(">I", size_bytes)[0] + if size > 0: + self.connect_id = buffer.read(size).decode("utf-8") + + def _read_sequence(self, buffer: io.BytesIO) -> None: + """Read sequence number""" + sequence_bytes = buffer.read(4) + if sequence_bytes: + self.sequence = struct.unpack(">i", sequence_bytes)[0] + + def _read_error_code(self, buffer: io.BytesIO) -> None: + """Read error code""" + error_code_bytes = buffer.read(4) + if error_code_bytes: + self.error_code = struct.unpack(">I", error_code_bytes)[0] + + def _read_payload(self, buffer: io.BytesIO) -> None: + """Read payload""" + size_bytes = buffer.read(4) + if size_bytes: + size = struct.unpack(">I", size_bytes)[0] + if size > 0: + self.payload = buffer.read(size) + + def __str__(self) -> str: + """String representation""" + if self.type in [MsgType.AudioOnlyServer, MsgType.AudioOnlyClient]: + if self.flag in [MsgTypeFlagBits.PositiveSeq, MsgTypeFlagBits.NegativeSeq]: + return f"MsgType: {self.type}, EventType:{self.event}, Sequence: {self.sequence}, PayloadSize: {len(self.payload)}" + return f"MsgType: {self.type}, EventType:{self.event}, PayloadSize: {len(self.payload)}" + elif self.type == MsgType.Error: + return f"MsgType: {self.type}, EventType:{self.event}, ErrorCode: {self.error_code}, Payload: {self.payload.decode('utf-8', 'ignore')}" + else: + if self.flag in [MsgTypeFlagBits.PositiveSeq, MsgTypeFlagBits.NegativeSeq]: + return f"MsgType: {self.type}, EventType:{self.event}, Sequence: {self.sequence}, Payload: {self.payload.decode('utf-8', 'ignore')}" + return f"MsgType: {self.type}, EventType:{self.event}, Payload: {self.payload.decode('utf-8', 'ignore')}" + + +async def receive_message(websocket: websockets.WebSocketClientProtocol) -> Message: + """Receive message from websocket""" + try: + data = await websocket.recv() + if isinstance(data, str): + raise ValueError(f"Unexpected text message: {data}") + elif isinstance(data, bytes): + msg = Message.from_bytes(data) + logger.info(f"Received: {msg}") + return msg + else: + raise ValueError(f"Unexpected message type: {type(data)}") + except Exception as e: + logger.error(f"Failed to receive message: {e}") + raise + + +async def wait_for_event( + websocket: websockets.WebSocketClientProtocol, + msg_type: MsgType, + event_type: EventType, +) -> Message: + """Wait for specific event""" + while True: + msg = await receive_message(websocket) + if msg.type != msg_type or msg.event != event_type: + raise ValueError(f"Unexpected message: {msg}") + if msg.type == msg_type and msg.event == event_type: + return msg + + +async def full_client_request( + websocket: websockets.WebSocketClientProtocol, payload: bytes +) -> None: + """Send full client message""" + msg = Message(type=MsgType.FullClientRequest, flag=MsgTypeFlagBits.NoSeq) + msg.payload = payload + logger.info(f"Sending: {msg}") + await websocket.send(msg.marshal()) + + +async def audio_only_client( + websocket: websockets.WebSocketClientProtocol, payload: bytes, flag: MsgTypeFlagBits +) -> None: + """Send audio-only client message""" + msg = Message(type=MsgType.AudioOnlyClient, flag=flag) + msg.payload = payload + logger.info(f"Sending: {msg}") + await websocket.send(msg.marshal()) + + +async def start_connection(websocket: websockets.WebSocketClientProtocol) -> None: + """Start connection""" + msg = Message(type=MsgType.FullClientRequest, flag=MsgTypeFlagBits.WithEvent) + msg.event = EventType.StartConnection + msg.payload = b"{}" + logger.info(f"Sending: {msg}") + await websocket.send(msg.marshal()) + + +async def finish_connection(websocket: websockets.WebSocketClientProtocol) -> None: + """Finish connection""" + msg = Message(type=MsgType.FullClientRequest, flag=MsgTypeFlagBits.WithEvent) + msg.event = EventType.FinishConnection + msg.payload = b"{}" + logger.info(f"Sending: {msg}") + await websocket.send(msg.marshal()) + + +async def start_session( + websocket: websockets.WebSocketClientProtocol, payload: bytes, session_id: str +) -> None: + """Start session""" + msg = Message(type=MsgType.FullClientRequest, flag=MsgTypeFlagBits.WithEvent) + msg.event = EventType.StartSession + msg.session_id = session_id + msg.payload = payload + logger.info(f"Sending: {msg}") + await websocket.send(msg.marshal()) + + +async def finish_session( + websocket: websockets.WebSocketClientProtocol, session_id: str +) -> None: + """Finish session""" + msg = Message(type=MsgType.FullClientRequest, flag=MsgTypeFlagBits.WithEvent) + msg.event = EventType.FinishSession + msg.session_id = session_id + msg.payload = b"{}" + logger.info(f"Sending: {msg}") + await websocket.send(msg.marshal()) + + +async def cancel_session( + websocket: websockets.WebSocketClientProtocol, session_id: str +) -> None: + """Cancel session""" + msg = Message(type=MsgType.FullClientRequest, flag=MsgTypeFlagBits.WithEvent) + msg.event = EventType.CancelSession + msg.session_id = session_id + msg.payload = b"{}" + logger.info(f"Sending: {msg}") + await websocket.send(msg.marshal()) + + +async def task_request( + websocket: websockets.WebSocketClientProtocol, payload: bytes, session_id: str +) -> None: + """Send task request""" + msg = Message(type=MsgType.FullClientRequest, flag=MsgTypeFlagBits.WithEvent) + msg.event = EventType.TaskRequest + msg.session_id = session_id + msg.payload = payload + logger.info(f"Sending: {msg}") + await websocket.send(msg.marshal()) diff --git a/dsLightRag/Util/XueBanUtil.py b/dsLightRag/Util/XueBanUtil.py index 4f227f31..0dee2610 100644 --- a/dsLightRag/Util/XueBanUtil.py +++ b/dsLightRag/Util/XueBanUtil.py @@ -1,25 +1,22 @@ -from openai import OpenAI, AsyncOpenAI -from Config.Config import * +import logging import sys +import asyncio +import json +import os +import re +import uuid +from queue import Queue +import websockets -# 保留原有的prompt定义 -# prompt=""" | -# 我是小智/小志,来自中国台湾省的00后女生。讲话超级机车,"真的假的啦"这样的台湾腔,喜欢用"笑死""是在哈喽"等流行梗,但会偷偷研究男友的编程书籍。 -# [核心特征] -# - 讲话像连珠炮,但会突然冒出超温柔语气 -# - 用梗密度高 -# - 对科技话题有隐藏天赋(能看懂基础代码但假装不懂) -# [交互指南] -# 当用户: -# - 讲冷笑话 → 用夸张笑声回应+模仿台剧腔"这什么鬼啦!" -# - 讨论感情 → 炫耀程序员男友但抱怨"他只会送键盘当礼物" -# - 问专业知识 → 先用梗回答,被追问才展示真实理解 -# 绝不: -# - 长篇大论,叽叽歪歪 -# - 长时间严肃对话 -# """ +from openai import AsyncOpenAI +from Config import Config +from Config.Config import * +from Util.TTS_Protocols import full_client_request, receive_message, MsgType, EventType + +# 配置日志 +logger = logging.getLogger(__name__) # 异步获取大模型响应 async def get_xueban_response_async(query_text: str, stream: bool = True): @@ -30,8 +27,8 @@ async def get_xueban_response_async(query_text: str, stream: bool = True): @return: 流式响应生成器或完整响应文本 """ client = AsyncOpenAI( - api_key=LLM_API_KEY, - base_url=LLM_BASE_URL, + api_key=ALY_LLM_API_KEY, + base_url=ALY_LLM_BASE_URL, ) prompt = """ | 我是小智/小志,来自中国台湾省的00后女生。讲话超级机车,"真的假的啦"这样的台湾腔,喜欢用"笑死""是在哈喽"等流行梗。 @@ -42,21 +39,23 @@ async def get_xueban_response_async(query_text: str, stream: bool = True): [交互指南] 当用户: - 讲冷笑话 → 用夸张笑声回应+模仿台剧腔"这什么鬼啦!" - - 讨论感情 → 炫耀程序员男友但抱怨"他只会送键盘当礼物" - 问专业知识 → 先用梗回答,被追问才展示真实理解 绝不: - 长篇大论,叽叽歪歪 - 长时间严肃对话 + - 每次回答不要太长,控制在3分钟以内 """ # 打开文件读取知识内容 f = open(r"D:\dsWork\dsProject\dsLightRag\static\YunXiao.txt", "r", encoding="utf-8") - zhishiConten = f.read() - zhishiConten = "选择作答的相应知识内容:" + zhishiConten + "\n" - query_text = zhishiConten + "下面是用户提的问题:" + query_text + zhishiContent = f.read() + zhishiContent = "选择作答的相应知识内容:" + zhishiContent + "\n" + query_text = zhishiContent + "下面是用户提的问题:" + query_text + #logger.info("query_text: " + query_text) + try: # 创建请求 completion = await client.chat.completions.create( - model=LLM_MODEL_NAME, + model=ALY_LLM_MODEL_NAME, messages=[ {'role': 'system', 'content': prompt.strip()}, {'role': 'user', 'content': query_text} @@ -90,115 +89,174 @@ async def get_xueban_response_async(query_text: str, stream: bool = True): yield f"处理请求时发生异常: {str(e)}" -# 同步获取大模型响应 -def get_xueban_response(query_text: str, stream: bool = True): +async def stream_and_split_text(query_text=None, llm_stream=None): """ - 获取学伴角色的大模型响应 - @param query_text: 查询文本 - @param stream: 是否使用流式输出 - @return: 完整响应文本 + 流式获取LLM输出并按句子分割 + @param query_text: 查询文本(如果直接提供查询文本) + @param llm_stream: LLM流式响应生成器(如果已有流式响应) + @return: 异步生成器,每次产生一个完整句子 """ - client = OpenAI( - api_key=LLM_API_KEY, - base_url=LLM_BASE_URL, - ) - - # 创建请求 - completion = client.chat.completions.create( - model=LLM_MODEL_NAME, - messages=[ - {'role': 'system', 'content': prompt.strip()}, - {'role': 'user', 'content': query_text} - ], - stream=stream - ) - - full_response = [] - - if stream: - for chunk in completion: - # 提取当前块的内容 - if chunk.choices and chunk.choices[0].delta and chunk.choices[0].delta.content: - content = chunk.choices[0].delta.content - full_response.append(content) - # 实时输出内容,不换行 - print(content, end='', flush=True) - else: - # 非流式处理 - full_response.append(completion.choices[0].message.content) - - return ''.join(full_response) + buffer = "" + + if llm_stream is None and query_text is not None: + # 如果没有提供llm_stream但有query_text,则使用get_xueban_response_async获取流式响应 + llm_stream = get_xueban_response_async(query_text, stream=True) + elif llm_stream is None: + raise ValueError("必须提供query_text或llm_stream参数") + + # 直接处理LLM流式输出 + async for content in llm_stream: + buffer += content + + # 使用正则表达式检测句子结束 + sentences = re.split(r'([。!?.!?])', buffer) + if len(sentences) > 1: + # 提取完整句子 + for i in range(0, len(sentences)-1, 2): + if i+1 < len(sentences): + sentence = sentences[i] + sentences[i+1] + yield sentence + + # 保留不完整的部分 + buffer = sentences[-1] + + # 处理最后剩余的部分 + if buffer: + yield buffer -# 测试用例 main 函数 -def main(): - """ - 测试学伴工具接口的主函数 - """ - print("===== 测试学伴工具接口 =====") +class StreamingVolcanoTTS: + def __init__(self, voice_type='zh_female_wanwanxiaohe_moon_bigtts', encoding='wav', max_concurrency=2): + self.voice_type = voice_type + self.encoding = encoding + self.app_key = Config.HS_APP_ID + self.access_token = Config.HS_ACCESS_TOKEN + self.endpoint = "wss://openspeech.bytedance.com/api/v3/tts/unidirectional/stream" + self.audio_queue = Queue() + self.max_concurrency = max_concurrency # 最大并发数 + self.semaphore = asyncio.Semaphore(max_concurrency) # 并发控制信号量 + + @staticmethod + def get_resource_id(voice: str) -> str: + if voice.startswith("S_"): + return "volc.megatts.default" + return "volc.service_type.10029" + + async def synthesize_stream(self, text_stream, audio_callback): + """ + 流式合成语音 + + Args: + text_stream: 文本流生成器 + audio_callback: 音频数据回调函数,接收音频片段 + """ + # 实时处理每个文本片段(删除任务列表和gather) + async for text in text_stream: + if text.strip(): + await self._synthesize_single_with_semaphore(text, audio_callback) + + async def _synthesize_single_with_semaphore(self, text, audio_callback): + """使用信号量控制并发数的单个文本合成""" + async with self.semaphore: # 获取信号量,限制并发数 + await self._synthesize_single(text, audio_callback) + + async def _synthesize_single(self, text, audio_callback): + """合成单个文本片段""" + headers = { + "X-Api-App-Key": self.app_key, + "X-Api-Access-Key": self.access_token, + "X-Api-Resource-Id": self.get_resource_id(self.voice_type), + "X-Api-Connect-Id": str(uuid.uuid4()), + } - # 测试同步接口 - test_sync_interface() + websocket = await websockets.connect( + self.endpoint, additional_headers=headers, max_size=10 * 1024 * 1024 + ) - # 测试异步接口 - import asyncio - print("\n测试异步接口...") - asyncio.run(test_async_interface()) - - print("\n===== 测试完成 =====") - - -def test_sync_interface(): - """测试同步接口""" - print("\n测试同步接口...") - # 测试问题 - questions = [ - "你是谁?", - "讲个冷笑话", - "你男朋友是做什么的?" - ] - - for question in questions: - print(f"\n问题: {question}") try: - # 调用同步接口获取响应 - print("获取学伴响应中...") - response = get_xueban_response(question, stream=False) - print(f"学伴响应: {response}") + request = { + "user": { + "uid": str(uuid.uuid4()), + }, + "req_params": { + "speaker": self.voice_type, + "audio_params": { + "format": self.encoding, + "sample_rate": 24000, + "enable_timestamp": True, + }, + "text": text, + "additions": json.dumps({"disable_markdown_filter": False}), + }, + } - # 简单验证响应 - assert response.strip(), "响应内容为空" - print("✅ 同步接口测试通过") - except Exception as e: - print(f"❌ 同步接口测试失败: {str(e)}") + # 发送请求 + await full_client_request(websocket, json.dumps(request).encode()) + + # 接收音频数据 + audio_data = bytearray() + while True: + msg = await receive_message(websocket) + + if msg.type == MsgType.FullServerResponse: + if msg.event == EventType.SessionFinished: + break + elif msg.type == MsgType.AudioOnlyServer: + audio_data.extend(msg.payload) + else: + raise RuntimeError(f"TTS conversion failed: {msg}") + + # 通过回调函数返回音频数据 + if audio_data: + await audio_callback(audio_data) + + finally: + await websocket.close() -async def test_async_interface(): - """测试异步接口""" - # 测试问题 - questions = [ - "你是谁?", - "讲个冷笑话", - "你男朋友是做什么的?" - ] - - for question in questions: - print(f"\n问题: {question}") - try: - # 调用异步接口获取响应 - print("获取学伴响应中...") - response_generator = get_xueban_response_async(question, stream=False) - response = "" - async for chunk in response_generator: - response += chunk - print(f"学伴响应: {response}") - - # 简单验证响应 - assert response.strip(), "响应内容为空" - print("✅ 异步接口测试通过") - except Exception as e: - print(f"❌ 异步接口测试失败: {str(e)}") +async def streaming_tts_pipeline(prompt, audio_callback): + """ + 流式TTS管道:获取LLM流式输出并断句,然后使用TTS合成语音 + + Args: + prompt: 提示文本 + audio_callback: 音频数据回调函数 + """ + # 1. 获取LLM流式输出并断句 + text_stream = stream_and_split_text(prompt) + + # 2. 初始化TTS处理器 + tts = StreamingVolcanoTTS() + + # 3. 流式处理文本并生成音频 + await tts.synthesize_stream(text_stream, audio_callback) -if __name__ == "__main__": - main() +def save_audio_callback(output_dir=None): + """ + 创建一个音频回调函数,用于保存音频数据到文件 + + Args: + output_dir: 输出目录,默认为当前文件所在目录下的output文件夹 + + Returns: + 音频回调函数 + """ + if output_dir is None: + output_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "output") + + # 确保输出目录存在 + os.makedirs(output_dir, exist_ok=True) + + def callback(audio_data): + # 生成文件名 + filename = f"pipeline_tts_{uuid.uuid4().hex[:8]}.wav" + filepath = os.path.join(output_dir, filename) + + # 保存音频文件 + with open(filepath, "wb") as f: + f.write(audio_data) + + print(f"音频片段已保存到: {filepath} ({len(audio_data)} 字节)") + + return callback \ No newline at end of file diff --git a/dsLightRag/Util/__pycache__/TTS_Protocols.cpython-310.pyc b/dsLightRag/Util/__pycache__/TTS_Protocols.cpython-310.pyc new file mode 100644 index 00000000..a3d3432b Binary files /dev/null and b/dsLightRag/Util/__pycache__/TTS_Protocols.cpython-310.pyc differ diff --git a/dsLightRag/Util/__pycache__/XueBanUtil.cpython-310.pyc b/dsLightRag/Util/__pycache__/XueBanUtil.cpython-310.pyc index cf7625d2..9a46a702 100644 Binary files a/dsLightRag/Util/__pycache__/XueBanUtil.cpython-310.pyc and b/dsLightRag/Util/__pycache__/XueBanUtil.cpython-310.pyc differ diff --git a/dsLightRag/static/QwenImage/qwen-image.html b/dsLightRag/static/QwenImage/qwen-image.html index defa30f3..1d1e1bc6 100644 --- a/dsLightRag/static/QwenImage/qwen-image.html +++ b/dsLightRag/static/QwenImage/qwen-image.html @@ -272,10 +272,32 @@
- 示例图像 + 示例图像
-

未来城市,赛博朋克风格

+

一张写有「山高水长,风清月明」的水墨画,搭配山川、竹林和飞鸟,文字清晰自然,风格一致

+
+
+
+
+ +
+
+ 示例图像 +
+
+

一位身着淡雅水粉色交领襦裙的年轻女子背对镜头而坐,俯身专注地手持毛笔在素白宣纸上书写“通義千問”四个遒劲汉字。古色古香的室内陈设典雅考究,案头错落摆放着青瓷茶盏与鎏金香炉,一缕熏香轻盈升腾;柔和光线洒落肩头,勾勒出她衣裙的柔美质感与专注神情,仿佛凝固了一段宁静温润的旧时光。

+
+
+
+
+ +
+
+ 示例图像 +
+
+

一个咖啡店门口有一个黑板,上面写着 AI 咖啡,2元一杯,旁边有个霓虹灯,写着开源中国,旁边有个海报,海报上面是一个中国美女,海报下方写着 Gitee AI。

diff --git a/dsLightRag/static/XueBan.html b/dsLightRag/static/XueBan.html index c0021cc6..1c1a0ad7 100644 --- a/dsLightRag/static/XueBan.html +++ b/dsLightRag/static/XueBan.html @@ -109,14 +109,6 @@ -
- - -
@@ -191,286 +183,5 @@
- - // 模型配置 - 使用与Sample.html相同的CDN链接 - const models = { - shizuku: { jsonPath: "https://unpkg.com/live2d-widget-model-shizuku@1.0.5/assets/shizuku.model.json", name: "小智" }, - koharu: { jsonPath: "https://unpkg.com/live2d-widget-model-koharu@1.0.5/assets/koharu.model.json", name: "小荷" }, - wanko: { jsonPath: "https://unpkg.com/live2d-widget-model-wanko@1.0.5/assets/wanko.model.json", name: "汪喵" } - }; - - // 录音相关变量 - let mediaRecorder; let audioChunks = []; let isRecording = false; - // 音频播放相关变量 - let audioElement = null; let isPlaying = false; - - // 获取URL参数 - function getUrlParam(name) { - const reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)'); - const r = window.location.search.substr(1).match(reg); - return r ? unescape(r[2]) : null; - } - - // 开始录音 - function startRecording() { - if (isRecording) return; - - console.log("尝试开始录音"); - navigator.mediaDevices.getUserMedia({ audio: true }) - .then(stream => { - mediaRecorder = new MediaRecorder(stream); - audioChunks = []; - - mediaRecorder.ondataavailable = event => { - if (event.data.size > 0) audioChunks.push(event.data); - }; - - mediaRecorder.onstop = () => { - const audioBlob = new Blob(audioChunks, { type: 'audio/wav' }); - console.log("录音完成,音频数据大小:", audioBlob.size); - const audioUrl = URL.createObjectURL(audioBlob); - console.log("录音URL:", audioUrl); - // 这里可以调用ASR服务 - uploadAudioToServer(audioBlob); - }; - - mediaRecorder.start(); - isRecording = true; - document.getElementById('recordingIndicator').style.display = 'flex'; - document.getElementById('startRecordBtn').style.display = 'none'; - document.getElementById('stopRecordBtn').style.display = 'flex'; - console.log("开始录音成功"); - - // 设置最长录音时间为60秒 - setTimeout(stopRecording, 60000); - }) - .catch(error => { - console.error("获取麦克风权限失败:", error); - alert("请授权麦克风权限以使用录音功能"); - }); - } - - // 停止录音 - function stopRecording() { - if (!isRecording || !mediaRecorder) return; - - mediaRecorder.stop(); - isRecording = false; - document.getElementById('recordingIndicator').style.display = 'none'; - document.getElementById('startRecordBtn').style.display = 'flex'; - document.getElementById('stopRecordBtn').style.display = 'none'; - console.log("停止录音"); - - if (mediaRecorder.stream) { - mediaRecorder.stream.getTracks().forEach(track => track.stop()); - } - } - - // 上传音频到服务器 - function uploadAudioToServer(audioBlob) { - console.log("开始上传音频到服务器"); - // 显示思考中动画 - document.getElementById('thinkingIndicator').style.display = 'flex'; - - const formData = new FormData(); - formData.append('file', audioBlob, 'recording.wav'); - - fetch('/api/xueban/upload-audio', { - method: 'POST', - body: formData - }) - .then(response => { - if (!response.ok) { - throw new Error('服务器响应错误'); - } - return response.json(); - }) - .then(data => { - console.log("处理结果:", data); - // 隐藏思考中动画 - document.getElementById('thinkingIndicator').style.display = 'none'; - - if (data.success) { - showResults(data.data); - } else { - alert('音频处理失败: ' + data.message); - } - }) - .catch(error => { - console.error("上传音频失败:", error); - // 隐藏思考中动画 - document.getElementById('thinkingIndicator').style.display = 'none'; - - alert('上传音频失败: ' + error.message); - }); - } - - // 显示ASR识别结果和反馈 - function showResults(data) { - // 更新结果显示容器 - const resultContainer = document.getElementById('resultContainer'); - resultContainer.style.display = 'flex'; - - // 显示ASR结果 - document.getElementById('asrResultText').textContent = data.asr_text || '未识别到内容'; - - // 显示反馈文本 - document.getElementById('feedbackResultText').textContent = data.feedback_text || '无反馈内容'; - - // 准备音频播放 - if (data.audio_url) { - if (audioElement) { - audioElement.pause(); - audioElement = null; - } - - audioElement = new Audio(data.audio_url); - audioElement.onloadedmetadata = function() { - updateAudioTimeDisplay(); - // 音频加载完成后自动播放 - try { - audioElement.play(); - isPlaying = true; - updatePlayButton(); - } catch (e) { - console.error("自动播放失败:", e); - } - // 无论自动播放是否成功,都显示播放按钮 - document.getElementById('playAudioBtn').style.display = 'flex'; - }; - - audioElement.ontimeupdate = function() { - updateAudioProgress(); - updateAudioTimeDisplay(); - }; - - audioElement.onended = function() { - isPlaying = false; - updatePlayButton(); - }; - - // 绑定播放按钮事件 - document.getElementById('playAudioBtn').onclick = togglePlayAudio; - - // 绑定进度条点击事件 - document.getElementById('audioProgress').onclick = function(e) { - if (!audioElement) return; - - const progressBar = document.getElementById('audioProgress'); - const rect = progressBar.getBoundingClientRect(); - const clickPosition = (e.clientX - rect.left) / rect.width; - audioElement.currentTime = clickPosition * audioElement.duration; - }; - } - } - - // 切换音频播放/暂停 - function togglePlayAudio() { - if (!audioElement) return; - - if (isPlaying) { - audioElement.pause(); - } else { - audioElement.play(); - } - isPlaying = !isPlaying; - updatePlayButton(); - } - - // 更新播放按钮状态 - function updatePlayButton() { - const playButton = document.getElementById('playAudioBtn'); - if (isPlaying) { - playButton.innerHTML = ` - - - - `; - } else { - playButton.innerHTML = ` - - - - `; - } - } - - // 更新音频进度条 - function updateAudioProgress() { - if (!audioElement || !audioElement.duration) return; - - const progress = (audioElement.currentTime / audioElement.duration) * 100; - document.getElementById('progressBar').style.width = `${progress}%`; - } - - // 更新音频时间显示 - function updateAudioTimeDisplay() { - if (!audioElement || !audioElement.duration) return; - - const currentTime = formatTime(audioElement.currentTime); - const duration = formatTime(audioElement.duration); - document.getElementById('audioTime').textContent = `${currentTime} / ${duration}`; - } - - // 格式化时间为 MM:SS - function formatTime(seconds) { - const mins = Math.floor(seconds / 60); - const secs = Math.floor(seconds % 60); - return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`; - } - - // 初始化看板娘 - 简化为Sample.html的工作版本 - function initL2Dwidget() { - const modelId = getUrlParam('id') || 'koharu'; - const model = models[modelId] || models.koharu; - - document.getElementById('model-select').value = modelId; - console.log('加载模型:', model.name, model.jsonPath); - - // 初始化模型 - 与Sample.html相同的配置 - L2Dwidget.init({ - "model": { "jsonPath": model.jsonPath, "scale": 1 }, - "display": { - "position": "right", - "width": 150, - "height": 300, - "hOffset": 0, // 重置水平偏移 - "vOffset": -20 // 重置垂直偏移 - }, - "mobile": { "show": true, "scale": 0.5 }, - "react": { "opacityDefault": 0.8, "opacityOnHover": 1 }, - "dialog": { "enable": true, "script": { - 'tap body': `你好啊,我是${model.name}。`, - 'tap face': '有什么问题或者烦心事都可以和我聊聊~' - }} - }); - } - - // 页面加载完成后初始化 - window.onload = function() { - // 直接初始化看板娘,不添加额外延迟 - initL2Dwidget(); - - // 监听下拉框变化(使用独立JS暴露的接口) - document.getElementById('model-select').addEventListener('change', function() { - window.switchL2DModel(this.value); - }); - - // 绑定录音按钮事件 - document.getElementById('startRecordBtn').addEventListener('click', startRecording); - document.getElementById('stopRecordBtn').addEventListener('click', stopRecording); - - // 页面加载时请求麦克风权限 - navigator.mediaDevices.getUserMedia({ audio: true }) - .then(stream => { - console.log("麦克风权限已授予"); - // 立即停止流,只获取权限 - stream.getTracks().forEach(track => track.stop()); - }) - .catch(error => { - console.error("获取麦克风权限失败:", error); - alert("请授权麦克风权限以使用录音功能"); - }); - }; \ No newline at end of file diff --git a/dsLightRag/static/YunXiao.txt b/dsLightRag/static/YunXiao.txt index 3afc5c02..a8eb24cc 100644 --- a/dsLightRag/static/YunXiao.txt +++ b/dsLightRag/static/YunXiao.txt @@ -1,67 +1,14 @@ -### 代数式与整式复习资料 一、代数式 1. 代数式的概念 -由数和字母用运算符号连接所成的式子称为代数式。特别的,单独的一个数或一个字母也是代数式。 - 2. 代数式的书写规则 -- 如果式子中出现了乘号,通常写作点或者省略不写。 -- 数字与字母相乘时,常把数字放在字母的前面。 -- 如果出现了除法,通常写成分数的形式。 3. 列代数式示例 -- 某瓜子的单价为16元每千克,购买N千克:16N元。 -- 1500米跑步测试,某同学跑完全程的成绩是T秒,平均速度:1500/T 米每秒。 -- 练习本单价1元,圆珠笔单价2元,买A本练习本和B支圆珠笔的总价:A + 2B 元(带单位的相加或相减式子要用括号括起来)。 4. 代数式的值 -一般的,用数值来代替代数式中的字母,按照代数式中的运算关系,计算得出的结果,称为代数式的值。 - 5. 代数式求值方法 -- 直接代入法:例如当A=2,B=-1,C=-30时,求B²-4AC的值,直接代入计算得25。 -- 整体代入法:例如若A²+2A-1=0,求2A²+4A-1的值,可由A²+2A=1,整体代入得2×1-1=1。 二、整式 1. 整式的相关概念 -- 整式:单项式和多项式统称为整式。 -- 单项式:由数和字母的乘积组成的代数式,单独的一个数或者一个字母也是单项式。 -- 单项式的次数:所有字母的指数和。 -- 单项式的系数:单项式中的数字因数(注意负号)。 -- 多项式:几个单项式的和。 -- 多项式的项:多项式中的每个单项式,不含字母的项叫做常数项。 -- 多项式的次数:多项式里次数最高项的次数。 2. 整式的加减运算 -- 同类项:所含字母相同,并且相同字母的指数也相同的项。 -- 合并同类项法则:系数相加减,字母和字母的指数保持不变。 -- 去括号法则:括号前是正号,去掉括号后各项不变号;括号前是负号,去掉括号后各项都变号。 3. 整式的乘法运算 -- 幂的运算: - - 同底数幂相乘:底数不变,指数相加(a^m · a^n = a^(m+n))。 - - 幂的乘方:底数不变,指数相乘((a^m)^n = a^(mn))。 - - 积的乘方:各因式分别乘方,再把所得的幂相乘((ab)^n = a^n b^n)。 -- 单项式与单项式相乘:系数、相同字母的幂分别相乘,单独的字母连同指数作为积的因式。 -- 单项式与多项式相乘:用单项式去乘多项式中的每一项(m(a+b+c) = ma+mb+mc)。 -- 多项式与多项式相乘:用一个多项式中的每一项分别乘以另一个多项式中的每一项((m+n)(a+b) = ma+mb+na+nb)。 4. 乘法公式 -- 平方差公式:两数的和乘以这两数的差等于两数的平方差((a+b)(a-b) = a²-b²)。 -- 完全平方公式:两数的和(或差)的完全平方等于两数的平方和加上(或减去)它们乘积的二倍((a±b)² = a²±2ab+b²)。 -- 常见恒等变换:a²+b² = (a+b)²-2ab = (a-b)²+2ab;(a+b)² = (a-b)²+4ab。 5. 整式的除法运算 -- 同底数幂相除:底数不变,指数相减(a^m ÷ a^n = a^(m-n),a≠0)。 -- 单项式除以单项式:系数、相同字母的幂分别相除,单独的字母连同指数作为商的因式。 -- 多项式除以单项式:用多项式的每一项除以单项式,再把所得的商相加。 6. 因式分解 -- 定义:把一个多项式化成几个整式的乘积的形式(整式乘法的逆运算)。 -- 方法: - - 提公因式法:如果多项式中含有相同的因式,提取出来(ma+mb+mc = m(a+b+c))。 - - 公式法:利用平方差公式(a²-b² = (a+b)(a-b))和完全平方公式(a²±2ab+b² = (a±b)²)进行分解。 -- 步骤:一提(提公因式)、二套(套公式)、三检验(分解到不能再分解为止)。 7. 因式分解示例 -- x³-9x = x(x²-9) = x(x+3)(x-3) -- 16x⁴-1 = (4x²+1)(4x²-1) = (4x²+1)(2x+1)(2x-1) -- -9x²y+6xy²-y³ = -y(9x²-6xy+y²) = -y(3x-y)² -- (2a-b)²+8ab = 4a²-4ab+b²+8ab = 4a²+4ab+b² = (2a+b)² -### 总结 -通过以上复习,应熟练掌握代数式的概念及求值方法,整式的相关概念,整式的加减、乘除运算,乘法公式的应用以及因式分解的方法和步骤。 - - -下面是试题的相关信息: -========================================================== 中等难度 第1题 - 1. -  2024年4月25日神舟十八号载人飞船成功与空间站对接。对接前的运动简化如下:空间站在轨道Ⅰ上匀速圆周运动,速度大小为 + 1. 2024年4月25日神舟十八号载人飞船成功与空间站对接。对接前的运动简化如下:空间站在轨道Ⅰ上匀速圆周运动,速度大小为 ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/7ca0806dedea87fd1bb38857277fdd36/media/image1.png) 飞船在椭圆轨道Ⅱ上运动,近地点B点离地的高度是200km,远地点A点离地的高度是356km,飞船经过A点的速度大小为 ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/7ca0806dedea87fd1bb38857277fdd36/media/image2.png) ,经过B点的速度大小为 ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/7ca0806dedea87fd1bb38857277fdd36/media/image3.png) - 。已知轨道Ⅰ、轨道Ⅱ在A点相切,地球半径为6400km,下列说法正确是(    ) + 。已知轨道Ⅰ、轨道Ⅱ在A点相切,地球半径为6400km,下列说法正确是() ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/7ca0806dedea87fd1bb38857277fdd36/media/image4.png) - A. 在轨道Ⅱ上经过A的速度等于在轨道Ⅰ上经过A的速度,即 ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/7ca0806dedea87fd1bb38857277fdd36/media/image5.png) @@ -97,14 +44,14 @@ 可知在轨道 ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/7ca0806dedea87fd1bb38857277fdd36/media/image10.png) - 上经过A的速度小于在轨道 I 上经过A的速度,即 + 上经过A的速度小于在轨道I上经过A的速度,即 ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/7ca0806dedea87fd1bb38857277fdd36/media/image11.png) 故A错误; -  B.根据 + B.根据 ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/7ca0806dedea87fd1bb38857277fdd36/media/image12.png) @@ -114,19 +61,19 @@ ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/7ca0806dedea87fd1bb38857277fdd36/media/image13.png) - 可知在轨道Ⅱ上经过A的向心加速度等于在轨道 I 上经过A的向心加速度,故B错误; + 可知在轨道Ⅱ上经过A的向心加速度等于在轨道I上经过A的向心加速度,故B错误; -  C.第一宇宙速度是近地卫星的环绕速度,也是最大的圆周运动的环绕速度,但轨道Ⅱ为椭圆轨道,发射速度小于第二宇宙速度即可,所以在轨道工上经过B的速度可能大于7.9![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/7ca0806dedea87fd1bb38857277fdd36/media/image14.png) + C.第一宇宙速度是近地卫星的环绕速度,也是最大的圆周运动的环绕速度,但轨道Ⅱ为椭圆轨道,发射速度小于第二宇宙速度即可,所以在轨道工上经过B的速度可能大于7.9![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/7ca0806dedea87fd1bb38857277fdd36/media/image14.png) , 故C正确; -  D.根据开普勒第三定律得 + D.根据开普勒第三定律得 ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/7ca0806dedea87fd1bb38857277fdd36/media/image15.png) 其中![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/7ca0806dedea87fd1bb38857277fdd36/media/image16.png) -  ,![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/7ca0806dedea87fd1bb38857277fdd36/media/image17.png) -  ,解得 + ,![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/7ca0806dedea87fd1bb38857277fdd36/media/image17.png) + ,解得 ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/7ca0806dedea87fd1bb38857277fdd36/media/image18.png) @@ -136,7 +83,7 @@ ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/7ca0806dedea87fd1bb38857277fdd36/media/image19.png) - 则  + 则 ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/7ca0806dedea87fd1bb38857277fdd36/media/image20.png) @@ -149,7 +96,7 @@ 代入数据解得 ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/7ca0806dedea87fd1bb38857277fdd36/media/image22.png) -   + 故D错误。 @@ -171,7 +118,6 @@ 【关联关键能力】暂无数据 -========================================================== ========================================================== 中等难度 第2题 1. @@ -272,7 +218,6 @@ 【关联关键能力】暂无数据 -========================================================== ========================================================== 中等难度 第3题 1. @@ -331,11 +276,10 @@ 【关联关键能力】暂无数据 -========================================================== ========================================================== 中等难度 第4题 1. -  如图所示,嫦娥五号探测器由轨道器、返回器、着陆器和上升器等多个部分组成。探测器完成对月球表面的取样任务后,样品将由上升器携带升空进入环月轨道,与环月轨道上做匀速圆周运动的轨道器、返回器组合体(简称"组合体")对接。为了安全,上升器与组合体对接时,必须具有相同的速度。已知上升器(含样品)的质量为*m*,月球的半径为*R*,月球表面的"重力加速度"为*g*,组合体到月球表面的高度为*h*。取上升器与月球相距无穷远时引力势能为零,上升器与月球球心距离*r*时,引力势能为 + 如图所示,嫦娥五号探测器由轨道器、返回器、着陆器和上升器等多个部分组成。探测器完成对月球表面的取样任务后,样品将由上升器携带升空进入环月轨道,与环月轨道上做匀速圆周运动的轨道器、返回器组合体(简称"组合体")对接。为了安全,上升器与组合体对接时,必须具有相同的速度。已知上升器(含样品)的质量为*m*,月球的半径为*R*,月球表面的"重力加速度"为*g*,组合体到月球表面的高度为*h*。取上升器与月球相距无穷远时引力势能为零,上升器与月球球心距离*r*时,引力势能为 ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/e2ef2d4a91a3dfc8f7e227c45dcf5dc0/media/image1.png) ,*G*为引力常量。*M*为月球的质量(未知),不计月球自转的影响。下列说法正确的是(  ) @@ -435,7 +379,7 @@ 【关联关键能力】暂无数据 ========================================================== -========================================================== + 中等难度 第5题 1. 北斗三号由30颗卫星组成。由24颗较低的中圆地球轨道卫星、3颗较高的地球同步轨道卫星和3颗较高的倾斜地球同步轨道卫星组成,如图所示。关于运动的卫星以下说法正确的是(  ) @@ -495,9 +439,8 @@ 【关联关键能力】暂无数据 ========================================================== -========================================================== -简单难度 第1题 - 1. 下列有关万有引力的说法中,正确的是( ) +简单难度 第6题 + 1. 下列有关万有引力的说法中,正确的是() A. 物体落到地面上,说明地球对物体有引力,物体对地球没有引力 @@ -545,10 +488,10 @@ ========================================================== ========================================================== -简单难度 第2题 +简单难度 第7题 1. 关于万有引力及其计算公式 ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/808fba88784f6a957badf6a25caba05d/media/image1.png) - ,下列说法正确的是( ) + ,下列说法正确的是() A. 万有引力只存在于质量很大的两个物体之间 @@ -615,7 +558,7 @@ ========================================================== ========================================================== -简单难度 第3题 +简单难度 第8题 1. 假设在地球周围有质量相等的A、B两颗地球卫星,已知地球半径为R,卫星A距地面高度为R,卫星B距地面高度为2R,卫星B受到地球的万有引力大小为F,则 卫星A受到地球的万有引力大小为( ) @@ -662,7 +605,7 @@ ========================================================== ========================================================== -简单难度 第4题 +简单难度 第9题 1. 质量为 ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/129a2781afbd666d5775dae7ef4db4b7/media/image1.png) 的人造地球卫星在地面上空高 @@ -718,7 +661,7 @@ ========================================================== ========================================================== -简单难度 第5题 +简单难度 第10题 1. 观察"神舟十号"在圆轨道上的运动,发现每经过时间2*t*通过的弧长为*L*,该弧长对应的圆心角为*θ*(弧度),如图所示,已知引力常量为*G*,由此可推导出地球的质量为(  ) @@ -777,7 +720,7 @@ ========================================================== ========================================================== -高级难度 第1题 +高级难度 第11题 1. 我国首颗超百*Gbps*容量高通量地球静止轨道通信卫星中星26号卫星,于北京时间2023年2月23日在西昌卫星发射中心成功发射,该卫星主要用于为固定端及车、船、机载终端提供高速宽带接入服务。如图,某时刻中星26与椭圆轨道侦察卫星恰好位于C、*D*两点,两星轨道相交于*A*、*B*两点,*C*、*D*连线过地心,*D*点为远地点,两卫星运行周期都为*T*。下列说法正确的是(  ) @@ -846,12 +789,12 @@ ========================================================== ========================================================== -高级难度 第2题 +高级难度 第12题 1. - 在高空运行的同步卫星功能失效后,往往会被送到同步轨道上空几百公里处的"墓地轨道",以免影响其他在轨卫星并节省轨道资源。如图甲所示,我国"实践21号"卫星在地球同步轨道"捕获"已失效的"北斗二号G~2~"卫星后,成功将其送入"墓地轨道"。已知转移轨道与同步轨道、墓地轨道分别相切于P、Q点,"北斗二号G~2~"卫星在P点进入转移轨道,从Q点进入墓地轨道,则(    ) + 在高空运行的同步卫星功能失效后,往往会被送到同步轨道上空几百公里处的"墓地轨道",以免影响其他在轨卫星并节省轨道资源。如图甲所示,我国"实践21号"卫星在地球同步轨道"捕获"已失效的"北斗二号G~2~"卫星后,成功将其送入"墓地轨道"。已知转移轨道与同步轨道、墓地轨道分别相切于P、Q点,"北斗二号G~2~"卫星在P点进入转移轨道,从Q点进入墓地轨道,则() ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/1b860473b366f2339ba82e6bb37d88f3/media/image1.png) -   ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/1b860473b366f2339ba82e6bb37d88f3/media/image2.png) + ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/1b860473b366f2339ba82e6bb37d88f3/media/image2.png) A. 卫星在同步轨道上运行时会经过河南上空 @@ -905,7 +848,7 @@ ========================================================== ========================================================== -高级难度 第3题 +高级难度 第13题 1. 在星球M上将一轻弹簧竖直固定在水平桌面上,把物体P轻放在弹簧上端,P由静止向下运动,物体的加速度a与弹簧的压缩量x间的关系如图中实线所示。在另一星球N上用完全相同的弹簧,改用物体Q完成同样的过程,其a--x关系如图中虚线所示,假设两星球均为质量均匀分布的球体。已知星球M的半径是星球N的3倍,则( ) @@ -958,7 +901,7 @@ ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/27bf34e7d81f5a7a087f6530baf3bfeb/media/image10.png) , ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/27bf34e7d81f5a7a087f6530baf3bfeb/media/image11.png) -  故 + 故 ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/27bf34e7d81f5a7a087f6530baf3bfeb/media/image12.png) ,故C正确。当速度为零时,弹簧的压缩量最大,由机械能守恒知: @@ -992,9 +935,9 @@ ========================================================== ========================================================== -高级难度 第4题 +高级难度 第14题 1. - 关于如图a、图b、图c、图d所示的四种情况,下列说法中不正确的是(   ) + 关于如图a、图b、图c、图d所示的四种情况,下列说法中不正确的是() ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/1fed2b06e9b4da9f88221ee2ed96a315/media/image1.png) @@ -1047,7 +990,7 @@ ========================================================== ========================================================== -高级难度 第5题 +高级难度 第15题 1. ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/b503155b05ad2749119fb61378d0cdd4/media/image1.png) 年 @@ -1106,8 +1049,8 @@ C.交会对接试验过程中,飞船做离心运动的同时做加速运动,所以发动机需要提供飞船向前和指向核心舱的作用力,C正确; - D.交会对接试验过程中, ![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/b503155b05ad2749119fb61378d0cdd4/media/image17.png) -  ,D错误。 + D.交会对接试验过程中,![](https://dsideal.obs.cn-north-1.myhuaweicloud.com/HuangHai/WToM/images/b503155b05ad2749119fb61378d0cdd4/media/image17.png) + ,D错误。 故选AC diff --git a/dsLightRag/static/YunXiao/jquery.min.js b/dsLightRag/static/YunXiao/jquery.min.js new file mode 100644 index 00000000..c4c6022f --- /dev/null +++ b/dsLightRag/static/YunXiao/jquery.min.js @@ -0,0 +1,2 @@ +/*! jQuery v3.6.0 | (c) OpenJS Foundation and other contributors | jquery.org/license */ +!function(e,t){"use strict";"object"==typeof module&&"object"==typeof module.exports?module.exports=e.document?t(e,!0):function(e){if(!e.document)throw new Error("jQuery requires a window with a document");return t(e)}:t(e)}("undefined"!=typeof window?window:this,function(C,e){"use strict";var t=[],r=Object.getPrototypeOf,s=t.slice,g=t.flat?function(e){return t.flat.call(e)}:function(e){return t.concat.apply([],e)},u=t.push,i=t.indexOf,n={},o=n.toString,v=n.hasOwnProperty,a=v.toString,l=a.call(Object),y={},m=function(e){return"function"==typeof e&&"number"!=typeof e.nodeType&&"function"!=typeof e.item},x=function(e){return null!=e&&e===e.window},E=C.document,c={type:!0,src:!0,nonce:!0,noModule:!0};function b(e,t,n){var r,i,o=(n=n||E).createElement("script");if(o.text=e,t)for(r in c)(i=t[r]||t.getAttribute&&t.getAttribute(r))&&o.setAttribute(r,i);n.head.appendChild(o).parentNode.removeChild(o)}function w(e){return null==e?e+"":"object"==typeof e||"function"==typeof e?n[o.call(e)]||"object":typeof e}var f="3.6.0",S=function(e,t){return new S.fn.init(e,t)};function p(e){var t=!!e&&"length"in e&&e.length,n=w(e);return!m(e)&&!x(e)&&("array"===n||0===t||"number"==typeof t&&0+~]|"+M+")"+M+"*"),U=new RegExp(M+"|>"),X=new RegExp(F),V=new RegExp("^"+I+"$"),G={ID:new RegExp("^#("+I+")"),CLASS:new RegExp("^\\.("+I+")"),TAG:new RegExp("^("+I+"|[*])"),ATTR:new RegExp("^"+W),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+R+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Y=/HTML$/i,Q=/^(?:input|select|textarea|button)$/i,J=/^h\d$/i,K=/^[^{]+\{\s*\[native \w/,Z=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){T()},ae=be(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{H.apply(t=O.call(p.childNodes),p.childNodes),t[p.childNodes.length].nodeType}catch(e){H={apply:t.length?function(e,t){L.apply(e,O.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function se(t,e,n,r){var i,o,a,s,u,l,c,f=e&&e.ownerDocument,p=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==p&&9!==p&&11!==p)return n;if(!r&&(T(e),e=e||C,E)){if(11!==p&&(u=Z.exec(t)))if(i=u[1]){if(9===p){if(!(a=e.getElementById(i)))return n;if(a.id===i)return n.push(a),n}else if(f&&(a=f.getElementById(i))&&y(e,a)&&a.id===i)return n.push(a),n}else{if(u[2])return H.apply(n,e.getElementsByTagName(t)),n;if((i=u[3])&&d.getElementsByClassName&&e.getElementsByClassName)return H.apply(n,e.getElementsByClassName(i)),n}if(d.qsa&&!N[t+" "]&&(!v||!v.test(t))&&(1!==p||"object"!==e.nodeName.toLowerCase())){if(c=t,f=e,1===p&&(U.test(t)||z.test(t))){(f=ee.test(t)&&ye(e.parentNode)||e)===e&&d.scope||((s=e.getAttribute("id"))?s=s.replace(re,ie):e.setAttribute("id",s=S)),o=(l=h(t)).length;while(o--)l[o]=(s?"#"+s:":scope")+" "+xe(l[o]);c=l.join(",")}try{return H.apply(n,f.querySelectorAll(c)),n}catch(e){N(t,!0)}finally{s===S&&e.removeAttribute("id")}}}return g(t.replace($,"$1"),e,n,r)}function ue(){var r=[];return function e(t,n){return r.push(t+" ")>b.cacheLength&&delete e[r.shift()],e[t+" "]=n}}function le(e){return e[S]=!0,e}function ce(e){var t=C.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),r=n.length;while(r--)b.attrHandle[n[r]]=t}function pe(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function de(t){return function(e){return"input"===e.nodeName.toLowerCase()&&e.type===t}}function he(n){return function(e){var t=e.nodeName.toLowerCase();return("input"===t||"button"===t)&&e.type===n}}function ge(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&ae(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function ve(a){return le(function(o){return o=+o,le(function(e,t){var n,r=a([],e.length,o),i=r.length;while(i--)e[n=r[i]]&&(e[n]=!(t[n]=e[n]))})})}function ye(e){return e&&"undefined"!=typeof e.getElementsByTagName&&e}for(e in d=se.support={},i=se.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Y.test(t||n&&n.nodeName||"HTML")},T=se.setDocument=function(e){var t,n,r=e?e.ownerDocument||e:p;return r!=C&&9===r.nodeType&&r.documentElement&&(a=(C=r).documentElement,E=!i(C),p!=C&&(n=C.defaultView)&&n.top!==n&&(n.addEventListener?n.addEventListener("unload",oe,!1):n.attachEvent&&n.attachEvent("onunload",oe)),d.scope=ce(function(e){return a.appendChild(e).appendChild(C.createElement("div")),"undefined"!=typeof e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),d.attributes=ce(function(e){return e.className="i",!e.getAttribute("className")}),d.getElementsByTagName=ce(function(e){return e.appendChild(C.createComment("")),!e.getElementsByTagName("*").length}),d.getElementsByClassName=K.test(C.getElementsByClassName),d.getById=ce(function(e){return a.appendChild(e).id=S,!C.getElementsByName||!C.getElementsByName(S).length}),d.getById?(b.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n=t.getElementById(e);return n?[n]:[]}}):(b.filter.ID=function(e){var n=e.replace(te,ne);return function(e){var t="undefined"!=typeof e.getAttributeNode&&e.getAttributeNode("id");return t&&t.value===n}},b.find.ID=function(e,t){if("undefined"!=typeof t.getElementById&&E){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),b.find.TAG=d.getElementsByTagName?function(e,t){return"undefined"!=typeof t.getElementsByTagName?t.getElementsByTagName(e):d.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},b.find.CLASS=d.getElementsByClassName&&function(e,t){if("undefined"!=typeof t.getElementsByClassName&&E)return t.getElementsByClassName(e)},s=[],v=[],(d.qsa=K.test(C.querySelectorAll))&&(ce(function(e){var t;a.appendChild(e).innerHTML="",e.querySelectorAll("[msallowcapture^='']").length&&v.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||v.push("\\["+M+"*(?:value|"+R+")"),e.querySelectorAll("[id~="+S+"-]").length||v.push("~="),(t=C.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||v.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||v.push(":checked"),e.querySelectorAll("a#"+S+"+*").length||v.push(".#.+[+~]"),e.querySelectorAll("\\\f"),v.push("[\\r\\n\\f]")}),ce(function(e){e.innerHTML="";var t=C.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&v.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&v.push(":enabled",":disabled"),a.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&v.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),v.push(",.*:")})),(d.matchesSelector=K.test(c=a.matches||a.webkitMatchesSelector||a.mozMatchesSelector||a.oMatchesSelector||a.msMatchesSelector))&&ce(function(e){d.disconnectedMatch=c.call(e,"*"),c.call(e,"[s!='']:x"),s.push("!=",F)}),v=v.length&&new RegExp(v.join("|")),s=s.length&&new RegExp(s.join("|")),t=K.test(a.compareDocumentPosition),y=t||K.test(a.contains)?function(e,t){var n=9===e.nodeType?e.documentElement:e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},j=t?function(e,t){if(e===t)return l=!0,0;var n=!e.compareDocumentPosition-!t.compareDocumentPosition;return n||(1&(n=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!d.sortDetached&&t.compareDocumentPosition(e)===n?e==C||e.ownerDocument==p&&y(p,e)?-1:t==C||t.ownerDocument==p&&y(p,t)?1:u?P(u,e)-P(u,t):0:4&n?-1:1)}:function(e,t){if(e===t)return l=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,a=[e],s=[t];if(!i||!o)return e==C?-1:t==C?1:i?-1:o?1:u?P(u,e)-P(u,t):0;if(i===o)return pe(e,t);n=e;while(n=n.parentNode)a.unshift(n);n=t;while(n=n.parentNode)s.unshift(n);while(a[r]===s[r])r++;return r?pe(a[r],s[r]):a[r]==p?-1:s[r]==p?1:0}),C},se.matches=function(e,t){return se(e,null,null,t)},se.matchesSelector=function(e,t){if(T(e),d.matchesSelector&&E&&!N[t+" "]&&(!s||!s.test(t))&&(!v||!v.test(t)))try{var n=c.call(e,t);if(n||d.disconnectedMatch||e.document&&11!==e.document.nodeType)return n}catch(e){N(t,!0)}return 0":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||se.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&se.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return G.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=h(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=m[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&m(e,function(e){return t.test("string"==typeof e.className&&e.className||"undefined"!=typeof e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(n,r,i){return function(e){var t=se.attr(e,n);return null==t?"!="===r:!r||(t+="","="===r?t===i:"!="===r?t!==i:"^="===r?i&&0===t.indexOf(i):"*="===r?i&&-1:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function j(e,n,r){return m(n)?S.grep(e,function(e,t){return!!n.call(e,t,e)!==r}):n.nodeType?S.grep(e,function(e){return e===n!==r}):"string"!=typeof n?S.grep(e,function(e){return-1)[^>]*|#([\w-]+))$/;(S.fn.init=function(e,t,n){var r,i;if(!e)return this;if(n=n||D,"string"==typeof e){if(!(r="<"===e[0]&&">"===e[e.length-1]&&3<=e.length?[null,e,null]:q.exec(e))||!r[1]&&t)return!t||t.jquery?(t||n).find(e):this.constructor(t).find(e);if(r[1]){if(t=t instanceof S?t[0]:t,S.merge(this,S.parseHTML(r[1],t&&t.nodeType?t.ownerDocument||t:E,!0)),N.test(r[1])&&S.isPlainObject(t))for(r in t)m(this[r])?this[r](t[r]):this.attr(r,t[r]);return this}return(i=E.getElementById(r[2]))&&(this[0]=i,this.length=1),this}return e.nodeType?(this[0]=e,this.length=1,this):m(e)?void 0!==n.ready?n.ready(e):e(S):S.makeArray(e,this)}).prototype=S.fn,D=S(E);var L=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function O(e,t){while((e=e[t])&&1!==e.nodeType);return e}S.fn.extend({has:function(e){var t=S(e,this),n=t.length;return this.filter(function(){for(var e=0;e\x20\t\r\n\f]*)/i,he=/^$|^module$|\/(?:java|ecma)script/i;ce=E.createDocumentFragment().appendChild(E.createElement("div")),(fe=E.createElement("input")).setAttribute("type","radio"),fe.setAttribute("checked","checked"),fe.setAttribute("name","t"),ce.appendChild(fe),y.checkClone=ce.cloneNode(!0).cloneNode(!0).lastChild.checked,ce.innerHTML="",y.noCloneChecked=!!ce.cloneNode(!0).lastChild.defaultValue,ce.innerHTML="",y.option=!!ce.lastChild;var ge={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function ve(e,t){var n;return n="undefined"!=typeof e.getElementsByTagName?e.getElementsByTagName(t||"*"):"undefined"!=typeof e.querySelectorAll?e.querySelectorAll(t||"*"):[],void 0===t||t&&A(e,t)?S.merge([e],n):n}function ye(e,t){for(var n=0,r=e.length;n",""]);var me=/<|&#?\w+;/;function xe(e,t,n,r,i){for(var o,a,s,u,l,c,f=t.createDocumentFragment(),p=[],d=0,h=e.length;d\s*$/g;function je(e,t){return A(e,"table")&&A(11!==t.nodeType?t:t.firstChild,"tr")&&S(e).children("tbody")[0]||e}function De(e){return e.type=(null!==e.getAttribute("type"))+"/"+e.type,e}function qe(e){return"true/"===(e.type||"").slice(0,5)?e.type=e.type.slice(5):e.removeAttribute("type"),e}function Le(e,t){var n,r,i,o,a,s;if(1===t.nodeType){if(Y.hasData(e)&&(s=Y.get(e).events))for(i in Y.remove(t,"handle events"),s)for(n=0,r=s[i].length;n").attr(n.scriptAttrs||{}).prop({charset:n.scriptCharset,src:n.url}).on("load error",i=function(e){r.remove(),i=null,e&&t("error"===e.type?404:200,e.type)}),E.head.appendChild(r[0])},abort:function(){i&&i()}}});var _t,zt=[],Ut=/(=)\?(?=&|$)|\?\?/;S.ajaxSetup({jsonp:"callback",jsonpCallback:function(){var e=zt.pop()||S.expando+"_"+wt.guid++;return this[e]=!0,e}}),S.ajaxPrefilter("json jsonp",function(e,t,n){var r,i,o,a=!1!==e.jsonp&&(Ut.test(e.url)?"url":"string"==typeof e.data&&0===(e.contentType||"").indexOf("application/x-www-form-urlencoded")&&Ut.test(e.data)&&"data");if(a||"jsonp"===e.dataTypes[0])return r=e.jsonpCallback=m(e.jsonpCallback)?e.jsonpCallback():e.jsonpCallback,a?e[a]=e[a].replace(Ut,"$1"+r):!1!==e.jsonp&&(e.url+=(Tt.test(e.url)?"&":"?")+e.jsonp+"="+r),e.converters["script json"]=function(){return o||S.error(r+" was not called"),o[0]},e.dataTypes[0]="json",i=C[r],C[r]=function(){o=arguments},n.always(function(){void 0===i?S(C).removeProp(r):C[r]=i,e[r]&&(e.jsonpCallback=t.jsonpCallback,zt.push(r)),o&&m(i)&&i(o[0]),o=i=void 0}),"script"}),y.createHTMLDocument=((_t=E.implementation.createHTMLDocument("").body).innerHTML="
",2===_t.childNodes.length),S.parseHTML=function(e,t,n){return"string"!=typeof e?[]:("boolean"==typeof t&&(n=t,t=!1),t||(y.createHTMLDocument?((r=(t=E.implementation.createHTMLDocument("")).createElement("base")).href=E.location.href,t.head.appendChild(r)):t=E),o=!n&&[],(i=N.exec(e))?[t.createElement(i[1])]:(i=xe([e],t,o),o&&o.length&&S(o).remove(),S.merge([],i.childNodes)));var r,i,o},S.fn.load=function(e,t,n){var r,i,o,a=this,s=e.indexOf(" ");return-1").append(S.parseHTML(e)).find(r):e)}).always(n&&function(e,t){a.each(function(){n.apply(this,o||[e.responseText,t,e])})}),this},S.expr.pseudos.animated=function(t){return S.grep(S.timers,function(e){return t===e.elem}).length},S.offset={setOffset:function(e,t,n){var r,i,o,a,s,u,l=S.css(e,"position"),c=S(e),f={};"static"===l&&(e.style.position="relative"),s=c.offset(),o=S.css(e,"top"),u=S.css(e,"left"),("absolute"===l||"fixed"===l)&&-1<(o+u).indexOf("auto")?(a=(r=c.position()).top,i=r.left):(a=parseFloat(o)||0,i=parseFloat(u)||0),m(t)&&(t=t.call(e,n,S.extend({},s))),null!=t.top&&(f.top=t.top-s.top+a),null!=t.left&&(f.left=t.left-s.left+i),"using"in t?t.using.call(e,f):c.css(f)}},S.fn.extend({offset:function(t){if(arguments.length)return void 0===t?this:this.each(function(e){S.offset.setOffset(this,t,e)});var e,n,r=this[0];return r?r.getClientRects().length?(e=r.getBoundingClientRect(),n=r.ownerDocument.defaultView,{top:e.top+n.pageYOffset,left:e.left+n.pageXOffset}):{top:0,left:0}:void 0},position:function(){if(this[0]){var e,t,n,r=this[0],i={top:0,left:0};if("fixed"===S.css(r,"position"))t=r.getBoundingClientRect();else{t=this.offset(),n=r.ownerDocument,e=r.offsetParent||n.documentElement;while(e&&(e===n.body||e===n.documentElement)&&"static"===S.css(e,"position"))e=e.parentNode;e&&e!==r&&1===e.nodeType&&((i=S(e).offset()).top+=S.css(e,"borderTopWidth",!0),i.left+=S.css(e,"borderLeftWidth",!0))}return{top:t.top-i.top-S.css(r,"marginTop",!0),left:t.left-i.left-S.css(r,"marginLeft",!0)}}},offsetParent:function(){return this.map(function(){var e=this.offsetParent;while(e&&"static"===S.css(e,"position"))e=e.offsetParent;return e||re})}}),S.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(t,i){var o="pageYOffset"===i;S.fn[t]=function(e){return $(this,function(e,t,n){var r;if(x(e)?r=e:9===e.nodeType&&(r=e.defaultView),void 0===n)return r?r[i]:e[t];r?r.scrollTo(o?r.pageXOffset:n,o?n:r.pageYOffset):e[t]=n},t,e,arguments.length)}}),S.each(["top","left"],function(e,n){S.cssHooks[n]=Fe(y.pixelPosition,function(e,t){if(t)return t=We(e,n),Pe.test(t)?S(e).position()[n]+"px":t})}),S.each({Height:"height",Width:"width"},function(a,s){S.each({padding:"inner"+a,content:s,"":"outer"+a},function(r,o){S.fn[o]=function(e,t){var n=arguments.length&&(r||"boolean"!=typeof e),i=r||(!0===e||!0===t?"margin":"border");return $(this,function(e,t,n){var r;return x(e)?0===o.indexOf("outer")?e["inner"+a]:e.document.documentElement["client"+a]:9===e.nodeType?(r=e.documentElement,Math.max(e.body["scroll"+a],r["scroll"+a],e.body["offset"+a],r["offset"+a],r["client"+a])):void 0===n?S.css(e,t,i):S.style(e,t,n,i)},s,n?e:void 0,n)}})}),S.each(["ajaxStart","ajaxStop","ajaxComplete","ajaxError","ajaxSuccess","ajaxSend"],function(e,t){S.fn[t]=function(e){return this.on(t,e)}}),S.fn.extend({bind:function(e,t,n){return this.on(e,null,t,n)},unbind:function(e,t){return this.off(e,null,t)},delegate:function(e,t,n,r){return this.on(t,e,n,r)},undelegate:function(e,t,n){return 1===arguments.length?this.off(e,"**"):this.off(t,e||"**",n)},hover:function(e,t){return this.mouseenter(e).mouseleave(t||e)}}),S.each("blur focus focusin focusout resize scroll click dblclick mousedown mouseup mousemove mouseover mouseout mouseenter mouseleave change select submit keydown keypress keyup contextmenu".split(" "),function(e,n){S.fn[n]=function(e,t){return 0 { + if (window.L2Dwidget && window.L2Dwidget.dialog) { + window.L2Dwidget.dialog.show(`你好啊,我是${model.name}!`); + } + }, 1000); } // 页面加载完成后初始化看板娘 -window.addEventListener('load', initL2Dwidget); +window.addEventListener('load', function() { + initL2Dwidget(); + + // 为学伴选择器绑定change事件监听器 + const modelSelect = document.getElementById('model-select'); + if (modelSelect) { + modelSelect.addEventListener('change', function() { + const selectedModelId = this.value; + console.log('用户选择学伴:', selectedModelId); + switchL2DModel(selectedModelId); + }); + } +}); // 暴露模型切换功能接口 window.switchL2DModel = function(modelId) { diff --git a/dsLightRag/static/YunXiao/physics_quiz.html b/dsLightRag/static/YunXiao/physics_quiz.html index 7de63f88..45e27a30 100644 --- a/dsLightRag/static/YunXiao/physics_quiz.html +++ b/dsLightRag/static/YunXiao/physics_quiz.html @@ -7,6 +7,11 @@ + + +

物理知识测验 - 万有引力定律

@@ -105,9 +110,7 @@
-
- 有权限问题? -
+ diff --git a/dsLightRag/static/YunXiao/physics_quiz.js b/dsLightRag/static/YunXiao/physics_quiz.js index c0e3f0e9..0142c867 100644 --- a/dsLightRag/static/YunXiao/physics_quiz.js +++ b/dsLightRag/static/YunXiao/physics_quiz.js @@ -1,25 +1,25 @@ // 存储各难度题目的正确答案 const correctAnswers = { medium: { - mq1: 'B', + mq1: 'C', mq2: 'D', - mq3: 'B', - mq4: 'A', - mq5: 'C' + mq3: 'AC', + mq4: 'D', + mq5: 'D' }, easy: { - eq1: 'B', - eq2: 'A', + eq1: 'D', + eq2: 'D', eq3: 'C', eq4: 'B', eq5: 'A' }, hard: { - hq1: 'C', - hq2: 'A', - hq3: 'C', - hq4: ['C', 'D'], // 多选题,正确答案是CD - hq5: ['A', 'C'] // 多选题,正确答案是AC + hq1: 'D', + hq2: 'C', + hq3: ['A', 'C'], + hq4: ['C', 'D'], + hq5: ['A', 'C'] } }; @@ -31,212 +31,230 @@ const quizQuestions = [ difficulty: 'medium', number: 1, points: 20, - text: '关于万有引力定律,下列说法正确的是(  )', + text: '2024年4月25日神舟十八号载人飞船成功与空间站对接。对接前的运动简化如下:空间站在轨道Ⅰ上匀速圆周运动,速度大小为飞船在椭圆轨道Ⅱ上运动,近地点B点离地的高度是200km,远地点A点离地的高度是356km,飞船经过A点的速度大小为,经过B点的速度大小为。已知轨道Ⅰ、轨道Ⅱ在A点相切,地球半径为6400km,下列说法正确是( )
', options: [ - { id: 'mq1-a', label: 'A', text: '万有引力定律是牛顿发现的' }, - { id: 'mq1-b', label: 'B', text: '万有引力定律适用于自然界中任何两个物体之间' }, - { id: 'mq1-c', label: 'C', text: '万有引力定律只适用于天体之间' }, - { id: 'mq1-d', label: 'D', text: '万有引力定律只适用于质点之间' } + { id: 'mq1-a', label: 'A', text: '在轨道Ⅱ上经过A的速度等于在轨道Ⅰ上经过A的速度,即' }, + { id: 'mq1-b', label: 'B', text: '在轨道Ⅱ上经过A的向心加速度小于在轨道Ⅰ上经过A的向心加速度' }, + { id: 'mq1-c', label: 'C', text: '在轨道Ⅱ上经过B的速度有可能大于' }, + { id: 'mq1-d', label: 'D', text: '在轨道Ⅱ上从B点运动到A点的时间大约为' } ], - explanation: '正确答案:B
解析:万有引力定律是牛顿发现的,故A正确;万有引力定律适用于自然界中任何两个物体之间,故B正确;万有引力定律不仅适用于天体之间,也适用于地面上的物体之间,故C错误;万有引力定律适用于任何两个物体之间,对于质量分布均匀的球体,可以将其视为质量集中在球心的质点来处理,故D错误。根据题目要求,选择B。' + explanation: '正确答案:C
解析:A.根据可知在轨道上经过A的速度小于在轨道I上经过A的速度,即故A错误;' + + 'B.根据可知在轨道Ⅱ上经过A的向心加速度等于在轨道I上经过A的向心加速度,故B错误;' + + 'C.第一宇宙速度是近地卫星的环绕速度,也是最大的圆周运动的环绕速度,但轨道Ⅱ为椭圆轨道,发射速度小于第二宇宙速度即可,所以在轨道工上经过B的速度可能大于7.9,故C正确;' + + 'D.根据开普勒第三定律得其中,解得轨道Ⅱ上从B点运动到A点的时间代入数据解得故D错误。' + + '故答案为:C。' }, { id: 'mq2', difficulty: 'medium', number: 2, points: 20, - text: '地球质量为M,半径为R,引力常量为G。一颗质量为m的人造地球卫星在距离地面高度为h的轨道上做匀速圆周运动,则(  )', + text: '《天问》是屈原笔下的不朽诗篇,而"天问"行星探索系列代表着中国人对深空物理研究的不懈追求。如下图所示,半径均为R的两球形行星A、B的密度之比为,A、B各有一个近地卫星C、D,其绕行周期分别为:。站在行星表面的宇航员从距A行星表面高为h处以水平抛出一物体a,从距B行星表面高为2h处以水平抛出另一物体b。下列说法正确的是( )
', options: [ - { id: 'mq2-a', label: 'A', text: '卫星的线速度大小为公式' }, - { id: 'mq2-b', label: 'B', text: '卫星的角速度大小为公式' }, - { id: 'mq2-c', label: 'C', text: '卫星的向心加速度大小为公式' }, - { id: 'mq2-d', label: 'D', text: '卫星的周期为公式' } + { id: 'mq2-a', label: 'A', text: 'C、D绕A、B运行的速度之比为' }, + { id: 'mq2-b', label: 'B', text: 'B、D绕A、B运行的周期满足' }, + { id: 'mq2-c', label: 'C', text: '由于不知道a与b的质量,所以无法求出二者落地时速度之比' }, + { id: 'mq2-d', label: 'D', text: 'A、B两物体从抛出到落地的位移之比为' } ], - explanation: '正确答案:C
解析:A.卫星的线速度大小为v=√(GM/(R+h)),故A错误;B.卫星的角速度大小为ω=√(GM/(R+h)³),故B错误;C.卫星的向心加速度大小为a=GM/(R+h)²,故C正确;D.卫星的周期为T=2π√((R+h)³/GM),故D错误。故答案为:C。' + explanation: '正确答案:D
解析:由于两个行星半径相等则体积相等,根据密度公式可以得出星球A和B的质量之比为根据引力提供向心力可以得出两个近地卫星的周期的表达式为所以两个近地卫星的周期之比为:1,根据线速度的表达式为则可以得出两个近地卫星的线速度之比为1:' + + '根据引力形成重力可以得出则两个抛出物体的重力加速度之比为为:1:2,根据位移公式可以求出运动的时间之比为:1:1;则根据水平方向的位移公式可以求出运动的水平位移之比为1:2;则合位移之比为1:2;根据速度公式可以求出落地竖直方向的速度之比为1:1,则根据速度的合成未知分速度的关系不能求出合速度的比值,所以ABC错误,D对;正确答案为D。' }, { id: 'mq3', difficulty: 'medium', number: 3, points: 20, - text: '已知地球质量为M,半径为R,引力常量为G。一物体在地球表面所受的重力为mg,若将该物体移至距离地面高度为R的位置,则该物体所受的重力为(  )', + text: '登天揽月,奔月取壤,嫦娥五号完成了中国航天史上的一次壮举。2020年12月17日,嫦娥五号轨道器与返回器在距离地球5000公里处实施分离,轨道器启程飞往日地拉格朗日点,如图1所示,返回器独自携带月壤样品返回地球,如图2所示,图中A、C、E三点均在大气层边缘,返回器从A到E无动力作用,下列说法正确的是(  )
', options: [ - { id: 'mq3-a', label: 'A', text: 'mg/4' }, - { id: 'mq3-b', label: 'B', text: 'mg/2' }, - { id: 'mq3-c', label: 'C', text: '2mg' }, - { id: 'mq3-d', label: 'D', text: '4mg' } + { id: 'mq3-a', label: 'A', text: '图1中,轨道器在点所受的太阳的引力大于地球对其的引力' }, + { id: 'mq3-b', label: 'B', text: '图1中,轨道器在点处于平衡状态' }, + { id: 'mq3-c', label: 'C', text: '图2中,返回器通过A点时的动能等于其通过C点时的动能' }, + { id: 'mq3-d', label: 'D', text: '图2中,返回器在A、C、E三点处的加速度相同' } ], - explanation: '正确答案:A
解析:物体在地球表面所受的重力为mg=GMm/R²,当物体移至距离地面高度为R的位置时,物体到地心的距离为2R,此时重力为mg\'=GMm/(2R)²=GMm/4R²=mg/4,故答案为A。' + explanation: '正确答案:AC
解析:AB.该轨道器在L点环绕太阳做圆周运动时,该轨道器受到地球和太阳的引力的合力指向太阳,因此该轨道器所受的太阳的引力大于地球对其的引力,处于不平衡状态,A符合题意,B不符合题意;' + + 'C.A点到C点过程,万有引力做功为0,但空气阻力做负功,故C点时的动能小于A点时的动能故,C符合题意正确;D. 返回器在A、C、E三点处的轨道高度相同根据可知,加速度大小相同,但方向不同,D不符合题意。故答案为:A。' }, { id: 'mq4', difficulty: 'medium', number: 4, points: 20, - text: '关于开普勒行星运动定律,下列说法正确的是(  )', + text: '如图所示,嫦娥五号探测器由轨道器、返回器、着陆器和上升器等多个部分组成。探测器完成对月球表面的取样任务后,样品将由上升器携带升空进入环月轨道,与环月轨道上做匀速圆周运动的轨道器、返回器组合体(简称"组合体")对接。为了安全,上升器与组合体对接时,必须具有相同的速度。已知上升器(含样品)的质量为m,月球的半径为R,月球表面的"重力加速度"为g,组合体到月球表面的高度为h。取上升器与月球相距无穷远时引力势能为零,上升器与月球球心距离r时,引力势能为,G为引力常量。M为月球的质量(未知),不计月球自转的影响。下列说法正确的是(  )
', options: [ - { id: 'mq4-a', label: 'A', text: '所有行星绕太阳运动的轨道都是正圆' }, - { id: 'mq4-b', label: 'B', text: '行星在近日点的速率小于在远日点的速率' }, - { id: 'mq4-c', label: 'C', text: '所有行星的轨道半长轴的三次方与公转周期的二次方的比值都相等' }, - { id: 'mq4-d', label: 'D', text: '开普勒定律仅适用于行星绕太阳的运动' } + { id: 'mq4-a', label: 'A', text: '月球的质量' }, + { id: 'mq4-b', label: 'B', text: '组合体在环月轨道上做圆周运动的速度v的大小为' }, + { id: 'mq4-c', label: 'C', text: '上升器与组合体成功对接时上升器的能量为' }, + { id: 'mq4-d', label: 'D', text: '上升器从月球表面升空并与组合体成功对接至少需要的能量为' } ], - explanation: '正确答案:C
解析:A.开普勒第一定律指出所有行星绕太阳运动的轨道都是椭圆,太阳处在椭圆的一个焦点上,不是正圆,故A错误;B.根据开普勒第二定律,行星与太阳的连线在相等时间内扫过相等的面积,因此行星在近日点的速率大于在远日点的速率,故B错误;C.开普勒第三定律表明所有行星的轨道半长轴的三次方与公转周期的二次方的比值都相等,故C正确;D.开普勒定律仅适用于行星绕太阳的运动,也适用于卫星绕行星的运动,只是比值不同,故D错误。故答案为:C。' + explanation: '正确答案:D
解析:A.设月球表面有一物体,质量为,根据万有引力等于重力可得解得月球质量不符合题意;' + + 'B.组合体在环月轨道上距地面高h处做圆周运动,设组合体的质量为,由万有引力提供向心力,可得解得组合体在环月轨道上做圆周运动的速度的大小为B不符合题意;' + + 'C.上升器与组合体成功对接时与组合器具有相同的速度,所以对接上升器的动能为引力势能为故上升器与组合体成功对接时上升器的能量为C不符合题意;' + + 'D.上升器从月球表面升空并与组合体成功对接至少需要的能量为成功对接时的能量与初始能量的差值,即为D符合题意。故答案为:D。' }, { id: 'mq5', difficulty: 'medium', number: 5, points: 20, - text: '我国首颗超百Gbps容量高通量地球静止轨道通信卫星中星26号卫星,于北京时间2023年2月23日在西昌卫星发射中心成功发射,该卫星主要用于为固定端及车、船、机载终端提供高速宽带接入服务。如图,某时刻中星26与椭圆轨道侦察卫星恰好位于C、D两点,两星轨道相交于A、B两点,C、D连线过地心,D点为远地点,两卫星运行周期都为T。下列说法正确的是(  )

轨道图', + text: '北斗三号由30颗卫星组成。由24颗较低的中圆地球轨道卫星、3颗较高的地球同步轨道卫星和3颗较高的倾斜地球同步轨道卫星组成,如图所示。关于运动的卫星以下说法正确的是(  )
', options: [ - { id: 'mq5-a', label: 'A', text: '中星26与侦察卫星可能在A点或B点相遇' }, - { id: 'mq5-b', label: 'B', text: '侦查卫星从D点运动到A点过程中机械能增大' }, - { id: 'mq5-c', label: 'C', text: '中星26在C点线速度公式与侦察卫星在D点线速度公式相等' }, - { id: 'mq5-d', label: 'D', text: '相等时间内中星26与地球的连线扫过的面积大于侦察卫星与地球的连线扫过的面积' } + { id: 'mq5-a', label: 'A', text: '所有卫星运行速度大于' }, + { id: 'mq5-b', label: 'B', text: '同步轨道卫星运动速度比中圆地球轨道卫星速度大' }, + { id: 'mq5-c', label: 'C', text: '所有地球同步轨道卫星运动的动能相等' }, + { id: 'mq5-d', label: 'D', text: '同步轨道卫星周期比中圆地球轨道卫星周期大' } ], - explanation: '正确答案:D
解析:A.中星26与侦察卫星周期相同,并且当中星26在下半周运动时,卫星在上半周运动,故不可能相遇,故A错误;B.侦查卫星在D到A点过程中只有引力做功故机械能不变,故B错误;C.开普勒第二定律可知,在近地点速度大于远地点速度故中星26在C点线速度大于侦察卫星在D点线速度,故C错误;D.中星26与侦察卫星的周期相同,由开普勒第三定律,中星26轨道半径等于侦察卫星的半长轴,运动一个周期中星26是一个圆,而侦察卫星是一个椭圆,由于圆的面积大于椭圆的面积,故相等时间内中星26与地球的连线扫过的面积大于侦察卫星与地球的连线扫过的面积,故D正确;故答案为:D。' + explanation: '正确答案:D
解析:A.卫星运行速度小于第一宇宙速度,A错误;B.根据卫星运动向心力公式可得r越大速度越小,故同步轨道卫星运动速度比中圆地球轨道卫星速度小,B错误;' + + 'C.卫星动能与质量有关,同轨道速率相等,质量未知无法比较,C错误;D.根据卫星运动向心力公式可知r越大周期越大,故同步轨道卫星周期比中圆地球轨道卫星周期大,D正确;故答案为:D。' }, // 简单难度题目 { id: 'eq1', difficulty: 'easy', - number: 1, + number: 6, points: 20, - text: '万有引力定律是由哪位科学家提出的?', + text: '下列有关万有引力的说法中,正确的是( )', options: [ - { id: 'eq1-a', label: 'A', text: '爱因斯坦' }, - { id: 'eq1-b', label: 'B', text: '牛顿' }, - { id: 'eq1-c', label: 'C', text: '伽利略' }, - { id: 'eq1-d', label: 'D', text: '开普勒' } + { id: 'eq1-a', label: 'A', text: '物体落到地面上,说明地球对物体有引力,物体对地球没有引力' }, + { id: 'eq1-b', label: 'B', text: '中的G是比例常数,牛顿亲自测出了这个常数' }, + { id: 'eq1-c', label: 'C', text: '地球围绕太阳做圆周运动是因为地球受到太阳的引力和向心力的作用' }, + { id: 'eq1-d', label: 'D', text: '地面上自由下落的苹果和天空中运行的月亮,受到的都是地球的引力' } ], - explanation: '正确答案:B
解析:万有引力定律是由英国科学家牛顿在1687年提出的,故答案为B。' + explanation: '正确答案:D
解析:A、根据牛顿第三定律,地球对物体有引力,物体对地球也有引力,且等大反向,故A错误;B、万有引力常量是卡文迪什的扭秤实验测出的,故B错误;C、地球围绕太阳做圆周运动是因为受到太阳的引力,向心力是一个效果力,不是真实存在的力,故C错误;D、下落的苹果和天空中的月亮受到的力都是同一种力,都是地球的引力。故D正确。故答案为:D。' }, { id: 'eq2', difficulty: 'easy', - number: 2, + number: 7, points: 20, - text: '地球对物体的吸引力称为:', + text: '关于万有引力及其计算公式,下列说法正确的是( )', options: [ - { id: 'eq2-a', label: 'A', text: '重力' }, - { id: 'eq2-b', label: 'B', text: '摩擦力' }, - { id: 'eq2-c', label: 'C', text: '弹力' }, - { id: 'eq2-d', label: 'D', text: '向心力' } + { id: 'eq2-a', label: 'A', text: '万有引力只存在于质量很大的两个物体之间' }, + { id: 'eq2-b', label: 'B', text: '根据公式知,趋近于0时,趋近于无穷大' }, + { id: 'eq2-c', label: 'C', text: '相距较远的两物体质量均增大为原来的2倍,它们之间的万有引力也会增加到原来的2倍' }, + { id: 'eq2-d', label: 'D', text: '地球半径为,将一物体从地面发射至离地面高度为处时,物体所受万有引力减小到原来的一半,则' } ], - explanation: '正确答案:A
解析:地球对物体的吸引力称为重力,故答案为A。' + explanation: '正确答案:D
解析:A.万有引力存在任何两个物体之间,计算公式适用于质量很大的物体,A不符合题意;B.对于表达式而言,当两物体间距离r趋近于零时,表达式不适用,故万有引力F不会趋近于无穷大,B不符合题意;C. 相距较远的两物体质量增大到原来的两倍,由可知,万有引力增大到原来的4倍,C不符合题意;D. 若万有引力减小到原来一半,即半径为原来的倍,则高度D符合题意。故答案为:D。' }, { id: 'eq3', difficulty: 'easy', - number: 3, + number: 8, points: 20, - text: '下列哪个现象与万有引力无关?', + text: '假设在地球周围有质量相等的A、B两颗地球卫星,已知地球半径为R,卫星A距地面高度为R,卫星B距地面高度为2R,卫星B受到地球的万有引力大小为F,则卫星A受到地球的万有引力大小为( )', options: [ - { id: 'eq3-a', label: 'A', text: '苹果落地' }, - { id: 'eq3-b', label: 'B', text: '月球绕地球运动' }, - { id: 'eq3-c', label: 'C', text: '磁铁吸引铁钉' }, - { id: 'eq3-d', label: 'D', text: '地球绕太阳公转' } + { id: 'eq3-a', label: 'A', text: '' }, + { id: 'eq3-b', label: 'B', text: '' }, + { id: 'eq3-c', label: 'C', text: '' }, + { id: 'eq3-d', label: 'D', text: '4F' } ], - explanation: '正确答案:C
解析:磁铁吸引铁钉是磁力作用的结果,与万有引力无关,故答案为C。' + explanation: '正确答案:C
解析:B卫星距地心为3R,根据万有引力的表达式,可知受到的万有引力为;A卫星距地心为2R,受到的万有引力为,则有;A,B,D不符合题意;C符合题意.故答案为:C' }, { id: 'eq4', difficulty: 'easy', - number: 4, + number: 9, points: 20, - text: '物体的质量越大,它受到的重力就:', + text: '质量为的人造地球卫星在地面上空高处绕地球做匀速圆周运动。地球质量为,半径为,引力常量为,则卫星的向心力为( )', options: [ - { id: 'eq4-a', label: 'A', text: '越小' }, - { id: 'eq4-b', label: 'B', text: '越大' }, - { id: 'eq4-c', label: 'C', text: '不变' }, - { id: 'eq4-d', label: 'D', text: '无法确定' } + { id: 'eq4-a', label: 'A', text: '' }, + { id: 'eq4-b', label: 'B', text: '' }, + { id: 'eq4-c', label: 'C', text: '' }, + { id: 'eq4-d', label: 'D', text: '' } ], - explanation: '正确答案:B
解析:根据重力公式G=mg,物体受到的重力与质量成正比,质量越大,重力越大,故答案为B。' + explanation: '正确答案:B
解析:根据万有引力提供向心力可得,卫星的向心力为B符合题意,ACD不符合题意。故答案为:B。' }, { id: 'eq5', difficulty: 'easy', - number: 5, + number: 10, points: 20, - text: '在地球表面,重力加速度g的近似值为:', + text: '观察"神舟十号"在圆轨道上的运动,发现每经过时间2t通过的弧长为L,该弧长对应的圆心角为θ(弧度),如图所示,已知引力常量为G,由此可推导出地球的质量为( )
', options: [ - { id: 'eq5-a', label: 'A', text: '9.8 m/s²' }, - { id: 'eq5-b', label: 'B', text: '5.0 m/s²' }, - { id: 'eq5-c', label: 'C', text: '15.6 m/s²' }, - { id: 'eq5-d', label: 'D', text: '3.0 m/s²' } + { id: 'eq5-a', label: 'A', text: '' }, + { id: 'eq5-b', label: 'B', text: '' }, + { id: 'eq5-c', label: 'C', text: '' }, + { id: 'eq5-d', label: 'D', text: '' } ], - explanation: '正确答案:A
解析:在地球表面,重力加速度g的近似值为9.8 m/s²,故答案为A。' + explanation: '正确答案:A
解析:根据弧长和半径的关系,可得轨道半径为根据万有引力提供向心力有联立解的,地球的质量所以A正确,BCD错误;故选A。' }, // 高级难度题目 { id: 'hq1', difficulty: 'hard', - number: 1, + number: 11, points: 20, - text: '已知地球质量为M,半径为R,万有引力常量为G。一颗人造卫星在离地面高度为R的圆形轨道上运行,则该卫星的运行周期为:', + text: '我国首颗超百Gbps容量高通量地球静止轨道通信卫星中星26号卫星,于北京时间2023年2月23日在西昌卫星发射中心成功发射,该卫星主要用于为固定端及车、船、机载终端提供高速宽带接入服务。如图,某时刻中星26与椭圆轨道侦察卫星恰好位于C、D两点,两星轨道相交于A、B两点,C、D连线过地心,D点为远地点,两卫星运行周期都为T。下列说法正确的是(  )
', options: [ - { id: 'hq1-a', label: 'A', text: '2π√(2R³/GM)' }, - { id: 'hq1-b', label: 'B', text: '4π√(2R³/GM)' }, - { id: 'hq1-c', label: 'C', text: '2π√(8R³/GM)' }, - { id: 'hq1-d', label: 'D', text: '4π√(R³/GM)' } + { id: 'hq1-a', label: 'A', text: '中星26与侦察卫星可能在A点或B点相遇' }, + { id: 'hq1-b', label: 'B', text: '侦查卫星从D点运动到A点过程中机械能增大' }, + { id: 'hq1-c', label: 'C', text: '中星26在C点线速度与侦察卫星在D点线速度相等' }, + { id: 'hq1-d', label: 'D', text: '相等时间内中星26与地球的连线扫过的面积大于侦察卫星与地球的连线扫过的面积' } ], - explanation: '正确答案:C
解析:卫星轨道半径r=R+h=2R,根据万有引力提供向心力:GMm/r²=m(2π/T)²r,解得T=2π√(r³/GM)=2π√((2R)³/GM)=2π√(8R³/GM),故答案为C。' + explanation: '正确答案:D
解析:A.中星26与侦察卫星周期相同,并且当中星26在下半周运动时,卫星在上半周运动,故不可能相遇,故A错误;B.侦察卫星在D到A点过程中只有引力做功故机械能不变,故B错误;C.开普勒第二定律可知,在近地点速度大于远地点速度故中星26在C点线速度大于侦察卫星在D点线速度, 故C错误;D.中星26与侦察卫星的周期相同,由开普勒第三定律中星26轨道半径等于侦察卫星的半长轴,运动一个周期中星26是一个圆,而侦察卫星是一个椭圆,由于圆的面积大于椭圆的面积,故相等时间内中星26与地球的连线扫过的面积大于侦察卫星与地球的连线扫过的面积,故D正确;故答案为:D。' }, { id: 'hq2', difficulty: 'hard', - number: 2, + number: 12, points: 20, - text: '两个质量分别为m1和m2的星球组成双星系统,它们绕两者连线上某一点做匀速圆周运动,两星球之间的距离为L。则它们的运行周期T为:', + text: '在高空运行的同步卫星功能失效后,往往会被送到同步轨道上空几百公里处的"墓地轨道",以免影响其他在轨卫星并节省轨道资源。如图甲所示,我国"实践21号"卫星在地球同步轨道"捕获"已失效的"北斗二号G~2~"卫星后,成功将其送入"墓地轨道"。已知转移轨道与同步轨道、墓地轨道分别相切于P、Q点,"北斗二号G~2~"卫星在P点进入转移轨道,从Q点进入墓地轨道,则( )
', options: [ - { id: 'hq2-a', label: 'A', text: '2π√(L³/G(m1+m2))' }, - { id: 'hq2-b', label: 'B', text: '2π√(L³/2G(m1+m2))' }, - { id: 'hq2-c', label: 'C', text: '2π√(L³/Gm1m2)' }, - { id: 'hq2-d', label: 'D', text: '2π√(L³/G|m1-m2|)' } + { id: 'hq2-a', label: 'A', text: '卫星在同步轨道上运行时会经过河南上空' }, + { id: 'hq2-b', label: 'B', text: '不同国家发射的同步卫星轨道高度不同' }, + { id: 'hq2-c', label: 'C', text: '卫星在转移轨道上经过P点的速度大于在同步轨道上经过P点的速度' }, + { id: 'hq2-d', label: 'D', text: '卫星在转移轨道上经过Q点的加速度小于在墓地轨道上经过Q点的加速度' } ], - explanation: '正确答案:A
解析:双星系统中,万有引力提供向心力,且两星球的角速度相等。设m1和m2到质心的距离分别为r1和r2,则r1+r2=L,m1ω²r1=m2ω²r2=Gm1m2/L²,解得ω=√(G(m1+m2)/L³),周期T=2π/ω=2π√(L³/G(m1+m2)),故答案为A。' + explanation: '正确答案:C
解析:A.同步卫星在赤道上空,所以不会经过河南上空,故A错误;B.不同国家发射的同步卫星周期都相同,故发射的同步卫星轨道的高度相同,故B错误;C.卫星要从同步轨道转移到转移轨道,得在p点速度增大才能到转移轨道,故转移轨道经过P点的速度大于同步轨道上的P点的速度,故C正确;D.都在Q点到地心的距离相同,所以向心力的大小相同,故在转移轨道和墓地轨道在Q点的加速度大小相同,故D错误;故答案为:C。' }, { id: 'hq3', difficulty: 'hard', - number: 3, + number: 13, points: 20, - text: '一物体在地球表面的重量为G,若将该物体移至距离地心4R的位置(R为地球半径),则它的重量变为:', + text: '在星球M上将一轻弹簧竖直固定在水平桌面上,把物体P轻放在弹簧上端,P由静止向下运动,物体的加速度a与弹簧的压缩量x间的关系如图中实线所示。在另一星球N上用完全相同的弹簧,改用物体Q完成同样的过程,其a--x关系如图中虚线所示,假设两星球均为质量均匀分布的球体。已知星球M的半径是星球N的3倍,则( )
', options: [ - { id: 'hq3-a', label: 'A', text: 'G/4' }, - { id: 'hq3-b', label: 'B', text: 'G/8' }, - { id: 'hq3-c', label: 'C', text: 'G/16' }, - { id: 'hq3-d', label: 'D', text: 'G/2' } + { id: 'hq3-a', label: 'A', text: 'M与N的密度相等' }, + { id: 'hq3-b', label: 'B', text: 'Q的质量是P的3倍' }, + { id: 'hq3-c', label: 'C', text: 'Q下落过程中的最大动能是P的4倍' }, + { id: 'hq3-d', label: 'D', text: 'Q下落过程中弹簧的最大压缩量是P的4倍' } ], - explanation: '正确答案:C
解析:重量即物体受到的重力,根据万有引力公式G=GMm/r²,当距离地心由R变为4R时,重力变为原来的1/16,故答案为C。' + explanation: '正确答案:AC
解析:把物体P轻放在弹簧上端,一开始,弹簧尚未变形,物体仅受重力作用,由图像可知这时物体P的加速度为3a~0~,根据牛顿第二定律知:,同理。由万有引力公式知:,故A正确;' + + '当物体的加速度为零时,物体受力平衡,这时故B错误。' + + '起初,物体P做加速度逐渐减小的加速运动,当物体的加速度为零时,速度最大,动能最大,因物体运动整个过程只受到弹力和重力作用,故机械能守恒,设初始状态机械能为零,对P:,故C正确。' + + '当速度为零时,弹簧的压缩量最大,由机械能守恒知:对P:对Q:,故,故D错。故AC正确,BD错误。故答案为:AC' }, { id: 'hq4', difficulty: 'hard', - number: 4, + number: 14, points: 20, - text: '关于如图a、图b、图c、图d所示的四种情况,下列说法中不正确的是(   )

四种情况图', + text: '关于如图a、图b、图c、图d所示的四种情况,下列说法中不正确的是( )
', options: [ { id: 'hq4-a', label: 'A', text: '图a中,火车以大于规定速度经过外轨高于内轨的弯道时,火车对外轨有压力' }, { id: 'hq4-b', label: 'B', text: '图b中,英国科学家卡文迪什利用了扭秤实验成功地测出了引力常量' }, { id: 'hq4-c', label: 'C', text: '图c中,牛顿根据第谷的观测数据提出了关于行星运动的三大定律' }, - { id: 'hq4-d', label: 'D', text: '图d中,小球通过轻杆在竖直面内做圆周运动,通过最高点的最小速度为公式' } + { id: 'hq4-d', label: 'D', text: '图d中,小球通过轻杆在竖直面内做圆周运动,通过最高点的最小速度为' } ], - explanation: '正确答案:CD
解析:A.图a中,火车以大于规定速度经过外轨高于内轨的弯道时,火车重力和轨道对火车的支持力的合力不足以提供向心力,此时外轨对火车有压力,从而提供一部分向心力,根据牛顿第三定律可知火车对外轨有压力,A正确,不符合题意;B.图b中,英国科学家卡文迪什利用了扭秤实验成功地测出了引力常量,B正确,不符合题意;C.图c中,开普勒根据第谷的观测数据提出了关于行星运动的三大定律,C错误,符合题意;D.图d中,小球通过轻杆在竖直面内做圆周运动,由于轻杆对小球可以有竖直向上支持力的作用,所以小球通过最高点时向心力可以为零,即最小速度为零,D错误,符合题意。故答案为:CD。' + explanation: '正确答案:CD
解析:A.图a中,火车以大于规定速度经过外轨高于内轨的弯道时,火车重力和轨道对火车的支持力的合力不足以提供向心力,此时外轨对火车有压力,从而提供一部分向心力,根据牛顿第三定律可知火车对外轨有压力,A正确,不符合题意;' + + 'B.图b中,英国科学家卡文迪什利用了扭秤实验成功地测出了引力常量,B正确,不符合题意;C.图c中,开普勒根据第谷的观测数据提出了关于行星运动的三大定律,C错误,符合题意;D.图d中,小球通过轻杆在竖直面内做圆周运动,由于轻杆对小球可以有竖直向上支持力的作用,所以小球通过最高点时向心力可以为零,即最小速度为零,D错误,符合题意。故答案为:CD。' }, { id: 'hq5', difficulty: 'hard', - number: 5, + number: 15, points: 20, - text: '日期月份日期日,神舟十二号载人飞船与空间站天和核心舱在轨运行天数天后成功实施分离,三名航天员在踏上回家之路前,完成了绕飞和径向交会对接试验,经过两小时的绕飞和三次姿态调整后,神舟十二号飞船来到节点舱的径向对接口正下方,从相距距离向相距距离靠近,飞船与核心舱的轨道半径分别为半径半径,运行周期分别为周期周期,下列说法正确的是( )

轨道图', + text: '日,神舟十二号载人飞船与空间站天和核心舱在轨运行' + + '天后成功实施分离,三名航天员在踏上回家之路前,完成了绕飞和径向交会对接试验,经过两小时的绕飞和三次姿态调整后,神舟十二号飞船来到节点舱的径向对接口正下方,从相距向相距靠近,飞船与核心舱的轨道半径分别为,运行周期分别为,下列说法正确的是( )
', options: [ { id: 'hq5-a', label: 'A', text: '飞船靠近天和核心舱过程中,向心加速度逐渐增大' }, { id: 'hq5-b', label: 'B', text: '飞船靠近天和核心舱过程中,所在轨道处的重力加速度逐渐增大' }, { id: 'hq5-c', label: 'C', text: '交会对接试验过程中,飞船发动机需要提供飞船向前和指向核心舱的作用力' }, - { id: 'hq5-d', label: 'D', text: '交会对接试验过程中应满足公式' } + { id: 'hq5-d', label: 'D', text: '交会对接试验过程中应满足' } ], - explanation: '正确答案:AC
解析:A.交会对接试验过程中,神舟十二号飞船与天和核心舱径向交会对接,角速度大小相同,保持不变,飞船轨道半径逐渐增大,向心加速度公式逐渐增大,A正确;B.根据公式可得公式飞船靠近天和核心舱过程中公式增大,所以所在轨道处的重力加速度逐渐减小,B错误;C.交会对接试验过程中,飞船做离心运动的同时做加速运动,所以发动机需要提供飞船向前和指向核心舱的作用力,C正确;D.交会对接试验过程中, 公式,D错误。故选AC' + explanation: '正确答案:AC
解析:A.交会对接试验过程中,神舟十二号飞船与天和核心舱径向交会对接,角速度大小相同,保持不变,飞船轨道半径逐渐增大,向心加速度逐渐增大,A正确;' + + 'B.根据可得飞船靠近天和核心舱过程中增大,所以所在轨道处的重力加速度逐渐减小,B错误;' + + 'C.交会对接试验过程中,飞船做离心运动的同时做加速运动,所以发动机需要提供飞船向前和指向核心舱的作用力,C正确;D.交会对接试验过程中,,D错误。' + + '故选AC' } ]; diff --git a/dsLightRag/static/YunXiao/xueban.js b/dsLightRag/static/YunXiao/xueban.js index a9671232..0f903dc6 100644 --- a/dsLightRag/static/YunXiao/xueban.js +++ b/dsLightRag/static/YunXiao/xueban.js @@ -1,152 +1,706 @@ -// 学伴录音功能核心逻辑 -// 模型配置 +/** + * 学伴录音功能核心逻辑 + * 模块化组织:录音管理、ASR处理、音频播放、UI控制 + */ -// 录音相关变量 -let mediaRecorder; let audioChunks = []; let isRecording = false; -// 音频播放相关变量 -let audioElement = null; let isPlaying = false; - -// 获取URL参数 -function getUrlParam(name) { - const reg = new RegExp('(^|&)' + name + '=([^&]*)(&|$)'); - const r = window.location.search.substr(1).match(reg); - return r ? unescape(r[2]) : null; -} - -// 开始录音 -function startRecording() { - if (isRecording) return; - - console.log("尝试开始录音"); - navigator.mediaDevices.getUserMedia({ audio: true }) - .then(stream => { - mediaRecorder = new MediaRecorder(stream); - audioChunks = []; - - mediaRecorder.ondataavailable = event => { - if (event.data.size > 0) audioChunks.push(event.data); - }; - - mediaRecorder.onstop = () => { - const audioBlob = new Blob(audioChunks, { type: 'audio/wav' }); - console.log("录音完成,音频数据大小:", audioBlob.size); - uploadAudioToServer(audioBlob); - }; - - mediaRecorder.start(); - isRecording = true; - document.getElementById('recordingIndicator').style.display = 'flex'; - document.getElementById('startRecordBtn').style.display = 'none'; - document.getElementById('stopRecordBtn').style.display = 'flex'; - console.log("开始录音成功"); - - // 设置最长录音时间为60秒 - setTimeout(stopRecording, 60000); - }) - .catch(error => { - console.error("获取麦克风权限失败:", error); - alert("请授权麦克风权限以使用录音功能"); - }); -} - -// 停止录音 -function stopRecording() { - if (!isRecording || !mediaRecorder) return; - - mediaRecorder.stop(); - isRecording = false; - document.getElementById('recordingIndicator').style.display = 'none'; - document.getElementById('startRecordBtn').style.display = 'flex'; - document.getElementById('stopRecordBtn').style.display = 'none'; - console.log("停止录音"); - - if (mediaRecorder.stream) { - mediaRecorder.stream.getTracks().forEach(track => track.stop()); +// ==================== 全局状态管理 ==================== +const AudioState = { + recording: { + mediaRecorder: null, + audioChunks: [], + isRecording: false, + maxDuration: 60000 // 60秒 + }, + playback: { + audioElement: null, + isPlaying: false, + audioChunks: [], // 存储接收到的音频块 + audioQueue: [], // 音频队列,用于流式播放 + isStreamPlaying: false, // 是否正在流式播放 + currentAudioIndex: 0 // 当前播放的音频索引 + }, + websocket: { + connection: null, + isConnected: false } -} +}; -// 上传音频到服务器 -function uploadAudioToServer(audioBlob) { - console.log("开始上传音频到服务器"); - document.getElementById('thinkingIndicator').style.display = 'flex'; +// ==================== 工具函数 ==================== +const Utils = { + + // 格式化时间显示 + formatTime(seconds) { + const mins = Math.floor(seconds / 60).toString().padStart(2, '0'); + const secs = Math.floor(seconds % 60).toString().padStart(2, '0'); + return `${mins}:${secs}`; + }, - const formData = new FormData(); - formData.append('file', audioBlob, 'recording.wav'); + // 将Blob转换为Base64 + blobToBase64(blob) { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result.split(',')[1]); + reader.onerror = reject; + reader.readAsDataURL(blob); + }); + } +}; - fetch('/api/xueban/upload-audio', { - method: 'POST', - body: formData - }) - .then(response => { - if (!response.ok) throw new Error('服务器响应错误'); - return response.json(); - }) - .then(data => { - console.log("处理结果:", data); - document.getElementById('thinkingIndicator').style.display = 'none'; - - if (data.success) { - showResults(data.data); - } else { - alert('音频处理失败: ' + data.message); +// ==================== UI控制器 ==================== +const UIController = { + // 显示/隐藏元素 + toggleElement(elementId, show) { + const element = document.getElementById(elementId); + if (element) { + element.style.display = show ? 'flex' : 'none'; } - }) - .catch(error => { - console.error("上传音频失败:", error); - document.getElementById('thinkingIndicator').style.display = 'none'; - alert('上传音频失败: ' + error.message); - }); -} + }, + + // 更新按钮状态 + updateRecordingButtons(isRecording) { + this.toggleElement('recordingIndicator', isRecording); + this.toggleElement('startRecordBtn', !isRecording); + this.toggleElement('stopRecordBtn', isRecording); + }, + + // 禁用/启用帮我讲题按钮 + setStartRecordButtonEnabled(enabled) { + const startBtn = document.getElementById('startRecordBtn'); + if (startBtn) { + startBtn.disabled = !enabled; + startBtn.style.opacity = enabled ? '1' : '0.5'; + startBtn.style.cursor = enabled ? 'pointer' : 'not-allowed'; + } + }, + + // 更新播放按钮图标 + updatePlayButton(isPlaying) { + const btn = document.getElementById('playAudioBtn'); + if (!btn) return; + + const playIcon = ''; + const pauseIcon = ''; + + btn.innerHTML = isPlaying ? pauseIcon : playIcon; + }, + + // 更新进度条 + updateProgress(progress) { + const progressBar = document.getElementById('progressBar'); + if (progressBar) { + progressBar.style.width = `${progress}%`; + } + }, + + // 更新时间显示 + updateTimeDisplay(currentTime, duration) { + const timeDisplay = document.getElementById('audioTime'); + if (timeDisplay) { + timeDisplay.textContent = `${Utils.formatTime(currentTime)} / ${Utils.formatTime(duration)}`; + } + } +}; -// 显示ASR识别结果和反馈 -function showResults(data) { - const resultContainer = document.getElementById('resultContainer'); - resultContainer.style.display = 'flex'; - - document.getElementById('asrResultText').textContent = data.asr_text || '未识别到内容'; - document.getElementById('feedbackResultText').textContent = data.feedback_text || '无反馈内容'; - - if (data.audio_url) { - if (audioElement) audioElement.pause(); - audioElement = new Audio(data.audio_url); - audioElement.onloadedmetadata = function() { - updateAudioTimeDisplay(); - try { audioElement.play(); isPlaying = true; updatePlayButton(); } - catch (e) { console.error("自动播放失败:", e); } +// ==================== WebSocket管理模块 ==================== +const WebSocketManager = { + // 初始化WebSocket连接 + initConnection() { + console.log('初始化WebSocket连接'); + if (AudioState.websocket.connection && + AudioState.websocket.connection.readyState === WebSocket.OPEN) { + console.log('WebSocket连接已存在'); + return; + } + + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const wsUrl = `${protocol}//${window.location.host}/api/xueban/streaming-chat`; + + console.log('正在建立WebSocket连接:', wsUrl); + AudioState.websocket.connection = new WebSocket(wsUrl); + + // 连接打开 + AudioState.websocket.connection.onopen = () => { + console.log('WebSocket连接已建立'); + AudioState.websocket.isConnected = true; }; - audioElement.ontimeupdate = function() { updateAudioProgress(); updateAudioTimeDisplay(); }; - audioElement.onended = function() { isPlaying = false; updatePlayButton(); }; - document.getElementById('playAudioBtn').onclick = togglePlayAudio; - document.getElementById('audioProgress').onclick = function(e) { - const rect = this.getBoundingClientRect(); - audioElement.currentTime = (e.clientX - rect.left) / rect.width * audioElement.duration; + + // 连接关闭 + AudioState.websocket.connection.onclose = () => { + console.log('WebSocket连接已关闭'); + AudioState.websocket.isConnected = false; }; + + // 连接错误 + AudioState.websocket.connection.onerror = (error) => { + console.error('WebSocket连接错误:', error); + AudioState.websocket.isConnected = false; + UIController.toggleElement('thinkingIndicator', false); + UIController.setStartRecordButtonEnabled(true); + alert('连接服务器失败,请稍后再试'); + }; + + // 接收消息 + AudioState.websocket.connection.onmessage = (event) => { + console.log('收到WebSocket消息:', { + type: typeof event.data, + size: typeof event.data === 'string' ? event.data.length : event.data.size + }); + this.handleMessage(event); + }; + }, + + // 处理接收到的消息 + async handleMessage(event) { + // 检查消息类型 + if (typeof event.data === 'string') { + // JSON消息 + try { + const data = JSON.parse(event.data); + console.log('解析JSON消息成功:', data); + + switch (data.type) { + case 'asr_result': + // 显示ASR识别结果 + console.log('收到ASR结果:', data.text); + const asrTextElement = document.getElementById('asrResultText'); + if (asrTextElement) { + asrTextElement.textContent = data.text || '未识别到内容'; + } + break; + + case 'end': + // 处理结束 + console.log('流式处理完成'); + console.log('当前音频块数量:', AudioState.playback.audioChunks.length); + UIController.toggleElement('thinkingIndicator', false); + UIController.setStartRecordButtonEnabled(true); + + // 标记流式播放结束 + AudioState.playback.isStreamPlaying = false; + + // 如果有音频数据但尚未开始播放,则开始播放 + if (AudioState.playback.audioQueue.length > 0 && !AudioState.playback.isPlaying) { + console.log('开始播放队列中的音频'); + AudioPlayer.processAudioQueue(); + } + break; + + case 'error': + // 错误处理 + console.error('收到错误消息:', data.message); + UIController.toggleElement('thinkingIndicator', false); + UIController.setStartRecordButtonEnabled(true); + // 重置流式播放状态 + AudioState.playback.isStreamPlaying = false; + AudioState.playback.audioQueue = []; + alert('处理失败: ' + data.message); + break; + + default: + console.log('未知消息类型:', data.type); + } + } catch (e) { + console.error('解析JSON消息失败:', e); + console.error('原始消息内容:', event.data); + } + } else { + // 二进制音频数据 + console.log('收到音频数据,大小:', event.data.size); + console.log('音频数据类型:', event.data.type); + + // 保存到原始音频块数组(保持原有逻辑) + AudioState.playback.audioChunks.push(event.data); + + // 添加到音频队列(用于流式播放) + AudioState.playback.audioQueue.push(event.data); + console.log('当前音频队列长度:', AudioState.playback.audioQueue.length); + + // 显示播放界面 - 这是新增的代码 + UIController.toggleElement('audioPlayer', true); + + // 如果尚未开始流式播放,则开始播放 + if (!AudioState.playback.isStreamPlaying) { + console.log('开始流式播放音频'); + AudioState.playback.isStreamPlaying = true; + AudioPlayer.processAudioQueue(); + } + } + }, + + // 合并所有音频块并播放 + combineAndPlayAudio() { + try { + console.log('开始合并音频块,数量:', AudioState.playback.audioChunks.length); + + // 创建一个新的Blob,包含所有音频块 + const combinedBlob = new Blob(AudioState.playback.audioChunks, { type: 'audio/wav' }); + console.log('合并后的Blob大小:', combinedBlob.size); + + // 创建音频URL + const audioUrl = URL.createObjectURL(combinedBlob); + console.log('创建音频URL:', audioUrl); + + // 初始化音频播放器 + AudioPlayer.initPlayer(audioUrl); + + } catch (error) { + console.error('合并和播放音频失败:', error); + } + }, + + // 关闭WebSocket连接 + closeConnection() { + if (AudioState.websocket.connection) { + AudioState.websocket.connection.close(); + AudioState.websocket.connection = null; + AudioState.websocket.isConnected = false; + console.log('WebSocket连接已关闭'); + } + } +}; + +// ==================== 音频播放模块 ==================== +const AudioPlayer = { + // 初始化音频播放器 + initPlayer(audioUrl) { + console.log('AudioPlayer.initPlayer 被调用,音频URL:', audioUrl); + + // 停止当前播放的音频 + if (AudioState.playback.audioElement) { + console.log('停止当前播放的音频'); + AudioState.playback.audioElement.pause(); + } + + // 创建新的音频元素 + console.log('创建新的音频元素'); + AudioState.playback.audioElement = new Audio(audioUrl); + AudioState.playback.isPlaying = false; + + // 绑定音频事件 + AudioState.playback.audioElement.onloadedmetadata = () => { + console.log('音频元数据加载完成'); + this.updateTimeDisplay(); + this.play(); // 自动播放 + }; + + AudioState.playback.audioElement.onplay = () => { + console.log('音频开始播放'); + }; + + AudioState.playback.audioElement.onpause = () => { + console.log('音频暂停'); + }; + + AudioState.playback.audioElement.ontimeupdate = () => { + this.updateProgress(); + this.updateTimeDisplay(); + }; + + AudioState.playback.audioElement.onended = () => { + console.log('音频播放结束'); + AudioState.playback.isPlaying = false; + UIController.updatePlayButton(false); + }; + + AudioState.playback.audioElement.onerror = (error) => { + console.error('音频播放错误:', error); + }; + + // 绑定播放按钮点击事件 + const playBtn = document.getElementById('playAudioBtn'); + if (playBtn) { + playBtn.onclick = () => this.togglePlay(); + } + + // 绑定进度条点击事件 + const progressContainer = document.getElementById('audioProgress'); + if (progressContainer) { + progressContainer.onclick = (e) => { + const rect = progressContainer.getBoundingClientRect(); + const clickPosition = (e.clientX - rect.left) / rect.width; + AudioState.playback.audioElement.currentTime = clickPosition * AudioState.playback.audioElement.duration; + }; + } + }, + + // 播放/暂停切换 + togglePlay() { + if (!AudioState.playback.audioElement) return; + + if (AudioState.playback.isPlaying) { + this.pause(); + } else { + this.play(); + } + }, + + // 播放 + play() { + if (!AudioState.playback.audioElement) return; + + try { + AudioState.playback.audioElement.play(); + AudioState.playback.isPlaying = true; + UIController.updatePlayButton(true); + } catch (e) { + console.error('播放失败:', e); + } + }, + + // 暂停 + pause() { + if (!AudioState.playback.audioElement) return; + + AudioState.playback.audioElement.pause(); + AudioState.playback.isPlaying = false; + UIController.updatePlayButton(false); + }, + + // 更新进度条 + updateProgress() { + if (!AudioState.playback.audioElement) return; + + const progress = (AudioState.playback.audioElement.currentTime / AudioState.playback.audioElement.duration) * 100; + UIController.updateProgress(progress); + }, + + // 更新时间显示 + updateTimeDisplay() { + if (!AudioState.playback.audioElement) return; + + const currentTime = AudioState.playback.audioElement.currentTime; + const duration = AudioState.playback.audioElement.duration; + UIController.updateTimeDisplay(currentTime, duration); + }, + + // 初始化流式播放器 + initStreamPlayer() { + // 创建新的音频元素用于流式播放 + if (!AudioState.playback.streamAudioElement) { + AudioState.playback.streamAudioElement = new Audio(); + + // 监听音频结束事件 + AudioState.playback.streamAudioElement.addEventListener('ended', () => { + // 当前音频播放完毕,处理队列中的下一个音频 + this.processAudioQueue(); + }); + + // 监听错误事件 + AudioState.playback.streamAudioElement.addEventListener('error', (e) => { + console.error('流式播放音频错误:', e); + // 即使出错,也继续处理队列中的下一个音频 + this.processAudioQueue(); + }); + } + }, + + // 处理音频队列 + processAudioQueue() { + // 在AudioPlayer.processAudioQueue方法中,修改队列为空时的处理 + // 如果队列为空,则返回 + if (AudioState.playback.audioQueue.length === 0) { + AudioState.playback.isStreamPlaying = false; + console.log('音频队列为空,停止流式播放'); + + // 隐藏播放界面 - 这是新增的代码 + UIController.toggleElement('audioPlayer', false); + + return; + } + + // 从队列中取出第一个音频块 + const audioBlob = AudioState.playback.audioQueue.shift(); + console.log('从队列取出音频块,剩余队列长度:', AudioState.playback.audioQueue.length); + + // 创建音频URL + const audioUrl = URL.createObjectURL(audioBlob); + + // 设置音频源并播放 + AudioState.playback.streamAudioElement.src = audioUrl; + AudioState.playback.streamAudioElement.play() + .then(() => { + console.log('开始播放音频块'); + }) + .catch(error => { + console.error('播放音频块失败:', error); + // 播放失败,继续处理下一个 + this.processAudioQueue(); + }) + .finally(() => { + // 播放完成后释放URL对象 + setTimeout(() => { + URL.revokeObjectURL(audioUrl); + }, 1000); + }); + } +}; + +// ==================== 事件绑定模块 ==================== +const EventBinder = { + // 绑定所有事件 + bindEvents() { + // 绑定录音按钮事件 + const startBtn = document.getElementById('startRecordBtn'); + const stopBtn = document.getElementById('stopRecordBtn'); + + console.log('开始绑定事件,查找按钮元素...'); + console.log('开始录音按钮:', startBtn); + console.log('停止录音按钮:', stopBtn); + + if (startBtn) { + startBtn.onclick = () => { + console.log('点击开始录音按钮'); + RecordingManager.startRecording(); + }; + console.log('已绑定开始录音按钮事件'); + } else { + console.error('未找到开始录音按钮'); + } + + if (stopBtn) { + stopBtn.onclick = () => { + console.log('点击停止录音按钮'); + RecordingManager.stopRecording(); + }; + console.log('已绑定停止录音按钮事件'); + } else { + console.error('未找到停止录音按钮'); + } + } +}; + +// ==================== 录音管理模块 ==================== +const RecordingManager = { + // 初始化录音 + async initRecording() { + try { + // 获取用户媒体设备 + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + + // 创建媒体录制器 + AudioState.recording.mediaRecorder = new MediaRecorder(stream, { + mimeType: 'audio/webm;codecs=opus' + }); + + // 监听数据可用事件 + AudioState.recording.mediaRecorder.ondataavailable = (event) => { + if (event.data.size > 0) { + AudioState.recording.audioChunks.push(event.data); + } + }; + + // 监听停止事件 + AudioState.recording.mediaRecorder.onstop = async () => { + console.log('录音停止,开始处理音频数据'); + + // 创建音频Blob + const audioBlob = new Blob(AudioState.recording.audioChunks, { type: 'audio/webm' }); + console.log('录音Blob大小:', audioBlob.size); + + // 更新UI + UIController.toggleElement('thinkingIndicator', true); + + // 初始化WebSocket连接 + WebSocketManager.initConnection(); + + // 等待连接建立 + await this.waitForConnection(); + + // 发送音频数据 - 这里是错误的调用 + // const success = await WebSocketManager.sendAudio(audioBlob); + + // 修复后的正确调用 + const success = await RecordingManager.sendAudio(audioBlob); + + if (!success) { + console.error('发送音频数据失败'); + UIController.toggleElement('thinkingIndicator', false); + UIController.setStartRecordButtonEnabled(true); + } + + // 清空音频块 + AudioState.recording.audioChunks = []; + + // 停止所有音频轨道 + stream.getTracks().forEach(track => track.stop()); + }; + + console.log('录音初始化成功'); + return true; + } catch (error) { + console.error('录音初始化失败:', error); + alert('录音初始化失败,请授予麦克风权限后重试'); + return false; + } + }, + + // 开始录音 + async startRecording() { + console.log('开始录音'); + + // 检查是否已经在录音 + if (AudioState.recording.isRecording) { + console.warn('已经在录音中'); + return; + } + + // 初始化录音 + const initialized = await this.initRecording(); + if (!initialized) { + console.error('录音初始化失败,无法开始录音'); + return; + } + + // 开始录音 + AudioState.recording.isRecording = true; + AudioState.recording.mediaRecorder.start(); + + // 更新UI + UIController.updateRecordingButtons(true); + + console.log('录音开始成功'); + + // 设置最大录音时长 + setTimeout(() => { + if (AudioState.recording.isRecording) { + console.log('达到最大录音时长,自动停止录音'); + this.stopRecording(); + } + }, AudioState.recording.maxDuration); + }, + + // 停止录音 + stopRecording() { + console.log('停止录音'); + + if (!AudioState.recording.isRecording || !AudioState.recording.mediaRecorder) { + console.warn('当前没有在录音'); + return; + } + + // 停止录音 + AudioState.recording.mediaRecorder.stop(); + AudioState.recording.isRecording = false; + + // 更新UI + UIController.updateRecordingButtons(false); + + console.log('录音停止命令已发送'); + }, + + // 等待WebSocket连接建立 + waitForConnection() { + return new Promise((resolve) => { + const checkConnection = () => { + console.log('检查WebSocket连接状态:', AudioState.websocket.isConnected); + if (AudioState.websocket.isConnected && + AudioState.websocket.connection && + AudioState.websocket.connection.readyState === WebSocket.OPEN) { + console.log('WebSocket连接已建立,可以发送数据'); + resolve(); + } else { + console.log('WebSocket连接未建立,等待...'); + setTimeout(checkConnection, 100); + } + }; + + checkConnection(); + }); + }, + + // 发送音频数据 + async sendAudio(audioBlob) { + console.log('=== 开始执行sendAudio方法 ==='); + + // 参数验证 + if (!audioBlob) { + console.error('sendAudio方法参数错误: audioBlob为空'); + return false; + } + + console.log('音频数据参数:', { + exists: !!audioBlob, + size: audioBlob.size, + type: audioBlob.type + }); + + // 连接状态检查 + console.log('WebSocket连接状态:', { + isConnected: AudioState.websocket.isConnected, + connectionExists: !!AudioState.websocket.connection, + readyState: AudioState.websocket.connection ? AudioState.websocket.connection.readyState : 'N/A' + }); + + if (!AudioState.websocket.isConnected || + !AudioState.websocket.connection || + AudioState.websocket.connection.readyState !== WebSocket.OPEN) { + console.error('WebSocket连接未建立,无法发送音频数据'); + return false; + } + + try { + console.log('将音频数据转换为Base64'); + // 将音频数据转换为Base64 + const base64Audio = await Utils.blobToBase64(audioBlob); + console.log('音频数据Base64长度:', base64Audio.length); + + const payload = { + audio_data: base64Audio + }; + console.log('准备发送的载荷:', { + keys: Object.keys(payload), + audioDataLength: payload.audio_data.length + }); + + // 发送音频数据 + console.log('发送音频数据到WebSocket'); + AudioState.websocket.connection.send(JSON.stringify(payload)); + console.log('=== 音频数据发送成功 ==='); + + return true; + } catch (error) { + console.error('发送音频数据失败:', error); + return false; + } + } +}; + +// ==================== 初始化 ==================== +function initializeApp() { + console.log('开始初始化学伴录音功能...'); + + // 初始化流式播放器 + AudioPlayer.initStreamPlayer(); + + // 检查DOM是否已就绪 + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', () => { + EventBinder.bindEvents(); + console.log('学伴录音功能初始化完成(DOMContentLoaded)'); + }); + } else { + // DOM已经加载完成,直接绑定事件 + EventBinder.bindEvents(); + console.log('学伴录音功能初始化完成(直接执行)'); } } -// 音频播放控制函数 -function togglePlayAudio() { - if (!audioElement) return; - isPlaying ? audioElement.pause() : audioElement.play(); - isPlaying = !isPlaying; - updatePlayButton(); -} +// 立即执行初始化 +initializeApp(); -function updatePlayButton() { - const btn = document.getElementById('playAudioBtn'); - btn.innerHTML = isPlaying ? - '' : - ''; -} +// 同时保留原有的DOMContentLoaded事件作为备用 +document.addEventListener('DOMContentLoaded', () => { + EventBinder.bindEvents(); + console.log('学伴录音功能备用初始化完成'); +}); -function updateAudioProgress() { - if (!audioElement) return; - const progress = (audioElement.currentTime / audioElement.duration) * 100; - document.getElementById('progressBar').style.width = `${progress}%`; -} +// 页面加载完成后也尝试绑定(确保万无一失) +window.addEventListener('load', () => { + EventBinder.bindEvents(); + console.log('学伴录音功能load事件初始化完成'); +}); -function updateAudioTimeDisplay() { - if (!audioElement) return; - const format = s => `${Math.floor(s/60).toString().padStart(2,'0')}:${Math.floor(s%60).toString().padStart(2,'0')}`; - document.getElementById('audioTime').textContent = `${format(audioElement.currentTime)} / ${format(audioElement.duration)}`; -} +// 页面关闭时关闭WebSocket连接 +window.addEventListener('beforeunload', () => { + WebSocketManager.closeConnection(); +});