From fc92a5feaf4c9d0437c802b0e9b9451aba031ecf Mon Sep 17 00:00:00 2001 From: jeremygan2021 Date: Tue, 3 Mar 2026 21:59:57 +0800 Subject: [PATCH] 1 --- config.py | 3 +- font.py | 189 ++---- main.py | 585 +++++++++++++++--- .../__pycache__/server.cpython-312.pyc | Bin 21697 -> 40459 bytes .../__pycache__/server.cpython-313.pyc | Bin 5071 -> 41719 bytes websocket_server/server.py | 421 ++++++++----- websocket_server/test_generated_thumb.bin | 154 +++++ 7 files changed, 963 insertions(+), 389 deletions(-) create mode 100644 websocket_server/test_generated_thumb.bin diff --git a/config.py b/config.py index cc0ef74..eef9a1f 100644 --- a/config.py +++ b/config.py @@ -18,7 +18,8 @@ NON_CAMERA.pins = { 'sck': 9, # SPI CLK / SCK 'dc': 46, # Data/Command 'rst': 11, # Reset - 'cs': 12 # Chip Select + 'cs': 12, # Chip Select + 'btn': 0 # Boot按键 } NON_CAMERA.audio = { 'enabled': True, diff --git a/font.py b/font.py index 42e67fd..488d886 100644 --- a/font.py +++ b/font.py @@ -2,29 +2,39 @@ import framebuf import struct import time import binascii +import gc class Font: def __init__(self, ws=None): self.ws = ws - self.cache = {} # Simple cache for font bitmaps: {code: bytes} + self.cache = {} + self.pending_requests = set() + self.retry_count = {} + self.max_retries = 3 def set_ws(self, ws): self.ws = ws + def clear_cache(self): + """清除字体缓存以释放内存""" + self.cache.clear() + gc.collect() + + def get_cache_size(self): + """获取当前缓存的字体数量""" + return len(self.cache) + def text(self, tft, text, x, y, color, bg=0x0000): - """ - Draw text on ST7789 display using WebSocket to fetch fonts - """ - # Pre-calculate color bytes + """在ST7789显示器上绘制文本""" + if not text: + return + color_bytes = struct.pack(">H", color) bg_bytes = struct.pack(">H", bg) - # Create LUT for current color/bg lut = [bytearray(16) for _ in range(256)] for i in range(256): for bit in range(8): - # bit 7 is first pixel (leftmost) - # target index: (7-bit)*2 val = (i >> bit) & 1 idx = (7 - bit) * 2 if val: @@ -36,7 +46,6 @@ class Font: initial_x = x - # 1. Identify missing fonts missing_codes = set() for char in text: if ord(char) > 127: @@ -44,12 +53,8 @@ class Font: if code not in self.cache: missing_codes.add(code) - # 2. Batch request missing fonts if missing_codes and self.ws: - # Convert to list for consistent order/string missing_list = list(missing_codes) - # Limit batch size? Maybe 20 chars at a time? - # For short ASR result, usually < 20 chars. req_str = ",".join([str(c) for c in missing_list]) print(f"Batch requesting fonts: {req_str}") @@ -58,16 +63,13 @@ class Font: self._wait_for_fonts(missing_codes) except Exception as e: print(f"Batch font request failed: {e}") - - # 3. Draw text + for char in text: - # Handle newlines if char == '\n': x = initial_x y += 16 continue - # Boundary check if x + 16 > tft.width: x = initial_x y += 16 @@ -77,121 +79,79 @@ class Font: is_chinese = False buf_data = None - # Check if it's Chinese (or non-ASCII) if ord(char) > 127: code = ord(char) if code in self.cache: buf_data = self.cache[code] is_chinese = True else: - # Still missing after batch request? - # Could be timeout or invalid char. - pass + if code in self.pending_requests: + retry = self.retry_count.get(code, 0) + if retry < self.max_retries: + self.retry_count[code] = retry + 1 + self._request_single_font(code) if is_chinese and buf_data: - # Draw Chinese character (16x16) self._draw_bitmap(tft, buf_data, x, y, 16, 16, lut) x += 16 else: - # Draw ASCII (8x16) using built-in framebuf font (8x8 actually) - # If char is not ASCII, replace with '?' to avoid framebuf errors if ord(char) > 127: char = '?' self._draw_ascii(tft, char, x, y, color, bg) x += 8 - + + def _request_single_font(self, code): + """请求单个字体""" + if self.ws: + try: + self.ws.send(f"GET_FONT_UNICODE:{code}") + except: + pass + def _wait_for_fonts(self, target_codes): - """ - Blocking wait for a set of font codes. - Buffers other messages to self.ws.unread_messages. - """ + """等待字体数据返回""" if not self.ws or not target_codes: return start = time.ticks_ms() self.local_deferred = [] - # 2 seconds timeout for batch - while time.ticks_diff(time.ticks_ms(), start) < 2000 and target_codes: - - # Check unread_messages first? - # Actually ws.recv() in our modified client already checks unread_messages. - # But wait, if we put something BACK into unread_messages, we need to be careful not to read it again immediately if we are looping? - # No, we only put NON-FONT messages back. We are looking for FONT messages. - # So if we pop a non-font message, we put it back? - # If we put it back at head, we will read it again next loop! Infinite loop! - # - # Solution: We should NOT use ws.recv() which pops from unread. - # We should assume unread_messages might contain what we need? - # - # Actually, `ws.recv()` pops from `unread_messages`. - # If we get a message that is NOT what we want, we should store it in a temporary list, and push them all back at the end? - # Or append to `unread_messages` (if it's a queue). - # But `unread_messages` is used as a LIFO or FIFO? - # pop(0) -> FIFO. - # If we append, it goes to end. - # So: - # 1. recv() -> gets msg. - # 2. Is it font? - # Yes -> process. - # No -> append to `temp_buffer`. - # 3. After function finishes (or timeout), extend `unread_messages` with `temp_buffer`? - # Wait, `unread_messages` should be preserved order. - # If we had [A, B] in unread. - # recv() gets A. Not font. Temp=[A]. - # recv() gets B. Not font. Temp=[A, B]. - # recv() gets network C (Font). Process. - # End. - # Restore: unread = Temp + unread? (unread is empty now). - # So unread becomes [A, B]. Correct. - - import uselect - - # Fast check if we can read - # But we want to block until SOMETHING arrives. - - # If unread_messages is not empty, we should process them first. - # But we can't peak easily without modifying recv again. - # Let's just use recv() and handle the buffering logic here. - + while time.ticks_diff(time.ticks_ms(), start) < 3000 and target_codes: try: - # Use a poller for the socket part to implement timeout - # But recv() handles logic. - # If unread_messages is empty, we poll socket. - can_read = False - if self.ws.unread_messages: + if hasattr(self.ws, 'unread_messages') and self.ws.unread_messages: can_read = True else: + import uselect poller = uselect.poll() poller.register(self.ws.sock, uselect.POLLIN) - events = poller.poll(100) # 100ms + events = poller.poll(100) if events: can_read = True if can_read: - msg = self.ws.recv() # This will pop from unread or read from sock + msg = self.ws.recv() if msg is None: - # Socket closed or error? - # Or just timeout in recv (but we polled). continue if isinstance(msg, str): - if msg == "FONT_BATCH_END": - # Batch complete. Mark remaining as failed. - # We need to iterate over a copy because we are modifying target_codes? - # Actually we just clear it. - # But wait, target_codes is passed by reference (set). - # If we clear it, loop breaks. - # But we also want to mark cache as None for missing ones. - temp_missing = list(target_codes) - for c in temp_missing: - print(f"Batch missing/failed: {c}") - self.cache[c] = None # Cache failure - target_codes.clear() + if msg.startswith("FONT_BATCH_END:"): + parts = msg[15:].split(":") + success = int(parts[0]) if len(parts) > 0 else 0 + failed = int(parts[1]) if len(parts) > 1 else 0 + if failed > 0: + temp_missing = list(target_codes) + for c in temp_missing: + if c not in self.cache: + print(f"Font failed after retries: {c}") + self.cache[c] = None + if c in target_codes: + target_codes.remove(c) + + target_codes.clear() + elif msg.startswith("FONT_DATA:"): - # General font data handler parts = msg.split(":") if len(parts) >= 3: try: @@ -205,60 +165,39 @@ class Font: self.cache[c] = d if c in target_codes: target_codes.remove(c) - # print(f"Batch loaded: {c}") + if c in self.retry_count: + del self.retry_count[c] except: pass else: - # Other message, e.g. START_PLAYBACK self.local_deferred.append(msg) elif msg is not None: - # Binary message? Buffer it too. self.local_deferred.append(msg) except Exception as e: print(f"Wait font error: {e}") - # End of wait. Restore deferred messages. if self.local_deferred: - # We want new_list = local_deferred + old_list - self.ws.unread_messages = self.local_deferred + self.ws.unread_messages + if hasattr(self.ws, 'unread_messages'): + self.ws.unread_messages = self.local_deferred + self.ws.unread_messages self.local_deferred = [] - def _wait_for_font(self, target_code_str): - # Compatibility wrapper or deprecated? - # The new logic uses batch wait. - pass - def _draw_bitmap(self, tft, bitmap, x, y, w, h, lut): - # Convert 1bpp bitmap to RGB565 buffer using LUT - - # Optimize buffer allocation - # bitmap length is w * h / 8 = 32 bytes for 16x16 - - # Create list of chunks + """绘制位图""" chunks = [lut[b] for b in bitmap] - - # Join chunks into one buffer rgb_buf = b''.join(chunks) - tft.blit_buffer(rgb_buf, x, y, w, h) def _draw_ascii(self, tft, char, x, y, color, bg): - # Use framebuf for ASCII + """绘制ASCII字符""" w, h = 8, 8 buf = bytearray(w * h // 8) fb = framebuf.FrameBuffer(buf, w, h, framebuf.MONO_VLSB) fb.fill(0) fb.text(char, 0, 0, 1) - # Since framebuf.text is 8x8, we center it vertically in 16px height - # Drawing pixel by pixel is slow but compatible - # To optimize, we can build a small buffer - - # Create a 8x16 RGB565 buffer rgb_buf = bytearray(8 * 16 * 2) - # Fill with background bg_high, bg_low = bg >> 8, bg & 0xFF color_high, color_low = color >> 8, color & 0xFF @@ -266,14 +205,10 @@ class Font: rgb_buf[i] = bg_high rgb_buf[i+1] = bg_low - # Draw the 8x8 character into the buffer (centered) - # MONO_VLSB: each byte is a column of 8 pixels - for col in range(8): # 0..7 + for col in range(8): byte = buf[col] - for row in range(8): # 0..7 + for row in range(8): if (byte >> row) & 1: - # Calculate position in rgb_buf - # Target: x=col, y=row+4 pos = ((row + 4) * 8 + col) * 2 rgb_buf[pos] = color_high rgb_buf[pos+1] = color_low diff --git a/main.py b/main.py index 9ab1d1e..5b0c66d 100644 --- a/main.py +++ b/main.py @@ -17,15 +17,27 @@ SERVER_IP = "6.6.6.88" SERVER_PORT = 8000 SERVER_URL = f"ws://{SERVER_IP}:{SERVER_PORT}/ws/audio" -# 图片接收状态 IMAGE_STATE_IDLE = 0 IMAGE_STATE_RECEIVING = 1 +UI_SCREEN_RECORDING = 1 +UI_SCREEN_CONFIRM = 2 +UI_SCREEN_RESULT = 3 + +BOOT_SHORT_MS = 500 +BOOT_LONG_MS = 2000 +BOOT_EXTRA_LONG_MS = 5000 + IMG_WIDTH = 120 IMG_HEIGHT = 120 +_last_btn_state = None +_btn_release_time = 0 +_btn_press_time = 0 + def connect_wifi(max_retries=5): + """连接WiFi网络""" wlan = network.WLAN(network.STA_IF) try: @@ -72,38 +84,183 @@ def connect_wifi(max_retries=5): return False -def print_asr(text, display=None): - print(f"ASR: {text}") - if display and display.tft: - display.fill_rect(0, 40, 240, 160, st7789.BLACK) - display.text(text, 0, 40, st7789.WHITE) +def draw_mic_icon(display, x, y, active=True): + """绘制麦克风图标""" + if not display or not display.tft: + return + + color = st7789.GREEN if active else st7789.DARKGREY + + display.tft.fill_rect(x + 5, y, 10, 5, color) + display.tft.fill_rect(x + 3, y + 5, 14, 10, color) + display.tft.fill_rect(x + 8, y + 15, 4, 8, color) + display.tft.fill_rect(x + 6, y + 23, 8, 2, color) + display.tft.fill_rect(x + 8, y + 25, 4, 3, color) +def draw_loading_spinner(display, x, y, angle, color=st7789.WHITE): + """绘制旋转加载图标""" + if not display or not display.tft: + return + + import math + rad = math.radians(angle) + + # Clear previous (simple erase) + # This is tricky without a buffer, so we just draw over. + # For better performance we should remember previous pos. + + center_x = x + 10 + center_y = y + 10 + radius = 8 + + for i in range(8): + theta = math.radians(i * 45) + rad + px = int(center_x + radius * math.cos(theta)) + py = int(center_y + radius * math.sin(theta)) + + # Brightness based on angle (simulated by color or size) + # Here we just draw dots + display.tft.pixel(px, py, color) + +def draw_check_icon(display, x, y): + """绘制勾选图标""" + if not display or not display.tft: + return + + display.tft.line(x, y + 5, x + 3, y + 8, st7789.GREEN) + display.tft.line(x + 3, y + 8, x + 10, y, st7789.GREEN) + + +def draw_progress_bar(display, x, y, width, height, progress, color=st7789.CYAN): + """绘制进度条""" + if not display or not display.tft: + return + + display.tft.fill_rect(x, y, width, height, st7789.DARKGREY) + if progress > 0: + bar_width = int(width * min(progress, 1.0)) + display.tft.fill_rect(x, y, bar_width, height, color) + + +def render_recording_screen(display, asr_text="", audio_level=0): + """渲染录音界面""" + if not display or not display.tft: + return + + display.tft.fill(st7789.BLACK) + + display.tft.fill_rect(0, 0, 240, 30, st7789.WHITE) + display.text("语音识别", 80, 8, st7789.BLACK) + + draw_mic_icon(display, 105, 50, True) + + if audio_level > 0: + bar_width = min(int(audio_level * 2), 200) + display.tft.fill_rect(20, 100, bar_width, 10, st7789.GREEN) + + if asr_text: + display.text(asr_text[:20], 20, 130, st7789.WHITE) + + display.tft.fill_rect(60, 200, 120, 25, st7789.RED) + display.text("松开停止", 85, 205, st7789.WHITE) + + +def render_confirm_screen(display, asr_text=""): + """渲染确认界面""" + if not display or not display.tft: + return + + display.tft.fill(st7789.BLACK) + + display.tft.fill_rect(0, 0, 240, 30, st7789.CYAN) + display.text("说完了吗?", 75, 8, st7789.BLACK) + + display.tft.fill_rect(10, 50, 220, 80, st7789.DARKGREY) + display.text(asr_text if asr_text else "未识别到文字", 20, 75, st7789.WHITE) + + display.tft.fill_rect(20, 150, 80, 30, st7789.GREEN) + display.text("短按确认", 30, 158, st7789.BLACK) + + display.tft.fill_rect(140, 150, 80, 30, st7789.RED) + display.text("长按重录", 155, 158, st7789.WHITE) + + +def render_result_screen(display, status="", prompt="", image_received=False): + """渲染结果界面""" + if not display or not display.tft: + return + + # Only clear if we are starting a new state or it's the first render + # But for simplicity we clear all for now. Optimizing this requires state tracking. + display.tft.fill(st7789.BLACK) + + # Header + display.tft.fill_rect(0, 0, 240, 30, st7789.WHITE) + display.text("AI 生成中", 80, 8, st7789.BLACK) + + if status == "OPTIMIZING": + display.text("正在思考...", 80, 60, st7789.CYAN) + display.text("优化提示词中", 70, 80, st7789.CYAN) + draw_progress_bar(display, 40, 110, 160, 6, 0.3, st7789.CYAN) + # Spinner will be drawn by main loop + + elif status == "RENDERING": + display.text("正在绘画...", 80, 60, st7789.YELLOW) + display.text("AI作画中", 85, 80, st7789.YELLOW) + draw_progress_bar(display, 40, 110, 160, 6, 0.7, st7789.YELLOW) + # Spinner will be drawn by main loop + + elif status == "COMPLETE" or image_received: + display.text("生成完成!", 80, 50, st7789.GREEN) + draw_check_icon(display, 110, 80) + + elif status == "ERROR": + display.text("生成失败", 80, 50, st7789.RED) + + if prompt: + display.tft.fill_rect(10, 140, 220, 50, 0x2124) # Dark Grey + display.text("提示词:", 15, 145, st7789.CYAN) + display.text(prompt[:25] + "..." if len(prompt) > 25 else prompt, 15, 165, st7789.WHITE) + + display.tft.fill_rect(60, 210, 120, 25, st7789.BLUE) + display.text("返回录音", 90, 215, st7789.WHITE) + def process_message(msg, display, image_state, image_data_list): - """处理WebSocket消息,返回新的image_state""" + """处理WebSocket消息""" + # Handle binary image data + if isinstance(msg, (bytes, bytearray)): + if image_state == IMAGE_STATE_RECEIVING: + image_data_list.append(msg) + # Optional: Update progress bar or indicator + return image_state, None + return image_state, None + if not isinstance(msg, str): - return image_state + return image_state, None + + status_info = None - # 处理ASR消息 if msg.startswith("ASR:"): print_asr(msg[4:], display) + return image_state, ("asr", msg[4:]) + + elif msg.startswith("STATUS:"): + parts = msg[7:].split(":", 1) + status_type = parts[0] + status_text = parts[1] if len(parts) > 1 else "" + print(f"Status: {status_type} - {status_text}") + return image_state, ("status", status_type, status_text) - # 处理图片生成状态消息 elif msg.startswith("GENERATING_IMAGE:"): - print(msg) - if display and display.tft: - display.fill_rect(0, 40, 240, 100, st7789.BLACK) - display.text("正在生成图片...", 0, 40, st7789.YELLOW) + # Deprecated by STATUS:RENDERING but kept for compatibility + return image_state, None - # 处理提示词优化消息 elif msg.startswith("PROMPT:"): prompt = msg[7:] print(f"Optimized prompt: {prompt}") - if display and display.tft: - display.fill_rect(0, 60, 240, 40, st7789.BLACK) - display.text("提示词: " + prompt[:20], 0, 60, st7789.CYAN) + return image_state, ("prompt", prompt) - # 处理图片开始消息 elif msg.startswith("IMAGE_START:"): try: parts = msg.split(":") @@ -111,64 +268,120 @@ def process_message(msg, display, image_state, image_data_list): img_size = int(parts[2]) if len(parts) > 2 else 64 print(f"Image start, size: {size}, img_size: {img_size}") image_data_list.clear() - image_data_list.append(img_size) # 保存图片尺寸 - return IMAGE_STATE_RECEIVING + image_data_list.append(img_size) # Store metadata at index 0 + return IMAGE_STATE_RECEIVING, None except Exception as e: print(f"IMAGE_START parse error: {e}") - return image_state + return image_state, None - # 处理图片数据消息 + # Deprecated text-based IMAGE_DATA handling elif msg.startswith("IMAGE_DATA:") and image_state == IMAGE_STATE_RECEIVING: try: data = msg.split(":", 1)[1] - image_data_list.append(data) + # Convert hex to bytes immediately if using old protocol, but we switched to binary + # Keep this just in case server rolls back? No, let's assume binary. + pass except: pass - # 处理图片结束消息 elif msg == "IMAGE_END" and image_state == IMAGE_STATE_RECEIVING: try: print("Image received, processing...") - # 获取图片尺寸 img_size = image_data_list[0] if image_data_list else 64 - hex_data = "".join(image_data_list[1:]) + # Combine all binary chunks (skipping metadata at index 0) + img_data = b"".join(image_data_list[1:]) image_data_list.clear() - # 将hex字符串转换为字节数据 - img_data = bytes.fromhex(hex_data) - print(f"Image data len: {len(img_data)}") - # 在屏幕中心显示图片 if display and display.tft: - # 计算居中位置 x = (240 - img_size) // 2 y = (240 - img_size) // 2 - - # 显示图片 display.show_image(x, y, img_size, img_size, img_data) - display.fill_rect(0, 0, 240, 30, st7789.WHITE) - display.text("图片已生成!", 0, 5, st7789.BLACK) + # Overlay success message slightly + display.tft.fill_rect(0, 0, 240, 30, st7789.WHITE) + display.text("图片已生成!", 70, 5, st7789.BLACK) gc.collect() print("Image displayed") + return IMAGE_STATE_IDLE, ("image_done",) except Exception as e: print(f"Image process error: {e}") + import sys + sys.print_exception(e) - return IMAGE_STATE_IDLE + return IMAGE_STATE_IDLE, None - # 处理图片错误消息 elif msg.startswith("IMAGE_ERROR:"): print(msg) - if display and display.tft: - display.fill_rect(0, 40, 240, 100, st7789.BLACK) - display.text("图片生成失败", 0, 40, st7789.RED) - return IMAGE_STATE_IDLE + return IMAGE_STATE_IDLE, ("error", msg[12:]) - return image_state + return image_state, None + + +def print_asr(text, display=None): + """打印ASR结果""" + print(f"ASR: {text}") + if display and display.tft: + display.fill_rect(0, 40, 240, 160, st7789.BLACK) + display.text(text, 0, 40, st7789.WHITE) + + +def get_boot_button_action(boot_btn): + """获取Boot按键动作类型 + + 返回: + 0: 无动作 + 1: 短按 (<500ms) + 2: 长按 (2-5秒) + 3: 超长按 (>5秒) + """ + global _last_btn_state, _btn_release_time, _btn_press_time + + current_value = boot_btn.value() + current_time = time.ticks_ms() + + if current_value == 0: + if _last_btn_state != 0: + _last_btn_state = 0 + _btn_press_time = current_time + return 0 + + if current_value == 1 and _last_btn_state == 0: + press_duration = time.ticks_diff(current_time, _btn_press_time) + _last_btn_state = 1 + + if press_duration < BOOT_SHORT_MS: + return 0 + elif press_duration < BOOT_LONG_MS: + return 1 + elif press_duration < BOOT_EXTRA_LONG_MS: + return 2 + else: + return 3 + + if _last_btn_state is None: + _last_btn_state = current_value + _btn_release_time = current_time + + return 0 + + +def check_memory(silent=False): + """检查内存使用情况 + + Args: + silent: 是否静默模式(不打印日志) + """ + free = gc.mem_free() + total = gc.mem_alloc() + free + usage = (gc.mem_alloc() / total) * 100 if total > 0 else 0 + if not silent: + print(f"Memory: {free} free, {usage:.1f}% used") + return usage def main(): @@ -191,12 +404,18 @@ def main(): if display.tft: display.init_ui() + ui_screen = UI_SCREEN_RECORDING is_recording = False ws = None image_state = IMAGE_STATE_IDLE image_data_list = [] + current_asr_text = "" + current_prompt = "" + current_status = "" + image_generation_done = False + confirm_waiting = False - def connect_ws(): + def connect_ws(force=False): nonlocal ws try: if ws: @@ -205,16 +424,24 @@ def main(): pass ws = None - try: - print(f"Connecting to {SERVER_URL}") - ws = WebSocketClient(SERVER_URL) - print("WebSocket connected!") - if display: - display.set_ws(ws) - return True - except Exception as e: - print(f"WS connection failed: {e}") - return False + retry_count = 0 + max_retries = 3 + + while retry_count < max_retries: + try: + print(f"Connecting to {SERVER_URL} (attempt {retry_count + 1})") + ws = WebSocketClient(SERVER_URL) + print("WebSocket connected!") + if display: + display.set_ws(ws) + + return True + except Exception as e: + print(f"WS connection failed: {e}") + retry_count += 1 + time.sleep(1) + + return False if connect_wifi(): connect_ws() @@ -222,27 +449,162 @@ def main(): print("Running in offline mode") read_buf = bytearray(4096) + last_audio_level = 0 + memory_check_counter = 0 + spinner_angle = 0 + last_spinner_time = 0 while True: try: - btn_val = boot_btn.value() + memory_check_counter += 1 - if btn_val == 0: - if not is_recording: - print(">>> Recording...") - is_recording = True + if memory_check_counter >= 300: + memory_check_counter = 0 + if check_memory(silent=True) > 80: + gc.collect() + print("Memory high, cleaned") + + # Spinner Animation + if ui_screen == UI_SCREEN_RESULT and not image_generation_done and current_status in ["OPTIMIZING", "RENDERING"]: + now = time.ticks_ms() + if time.ticks_diff(now, last_spinner_time) > 100: if display.tft: - display.fill(st7789.WHITE) + # Clear previous spinner (draw in BLACK) + draw_loading_spinner(display, 110, 80, spinner_angle, st7789.BLACK) + + spinner_angle = (spinner_angle + 45) % 360 + + # Draw new spinner + color = st7789.CYAN if current_status == "OPTIMIZING" else st7789.YELLOW + draw_loading_spinner(display, 110, 80, spinner_angle, color) - if ws is None or not ws.is_connected(): - connect_ws() + last_spinner_time = now + + btn_action = get_boot_button_action(boot_btn) + + if btn_action == 1: + if is_recording: + print(">>> Stop recording") + if ws and ws.is_connected(): + try: + ws.send("STOP_RECORDING") + except: + ws = None + is_recording = False + ui_screen = UI_SCREEN_RESULT + image_generation_done = False + + if display.tft: + render_result_screen(display, "OPTIMIZING", current_asr_text, False) + + time.sleep(0.5) + + elif ui_screen == UI_SCREEN_RECORDING: + if not is_recording: + print(">>> Recording...") + is_recording = True + confirm_waiting = False + current_asr_text = "" + current_prompt = "" + current_status = "" + image_generation_done = False + + if display.tft: + render_recording_screen(display, "", 0) + + if ws is None or not ws.is_connected(): + connect_ws() + + if ws and ws.is_connected(): + try: + ws.send("START_RECORDING") + except: + ws = None + + elif ui_screen == UI_SCREEN_CONFIRM: + print(">>> Confirm and generate") + if ws and ws.is_connected(): + try: + ws.send("STOP_RECORDING") + except: + ws = None + + is_recording = False + ui_screen = UI_SCREEN_RESULT + image_generation_done = False + + if display.tft: + render_result_screen(display, "OPTIMIZING", current_asr_text, False) + + time.sleep(0.5) + + elif ui_screen == UI_SCREEN_RESULT: + print(">>> Back to recording") + ui_screen = UI_SCREEN_RECORDING + is_recording = False + current_asr_text = "" + current_prompt = "" + current_status = "" + image_generation_done = False + confirm_waiting = False + + if display.tft: + render_recording_screen(display, "", 0) + + elif btn_action == 2: + if is_recording: + print(">>> Stop recording (long press)") + if ws and ws.is_connected(): + try: + ws.send("STOP_RECORDING") + except: + ws = None + + is_recording = False + + if ui_screen == UI_SCREEN_RECORDING or is_recording == False: + if current_asr_text: + print(">>> Generate image with ASR text") + ui_screen = UI_SCREEN_RESULT + image_generation_done = False + + if display.tft: + render_result_screen(display, "OPTIMIZING", current_asr_text, False) + + time.sleep(0.5) + else: + print(">>> Re-record") + current_asr_text = "" + confirm_waiting = False + ui_screen = UI_SCREEN_RECORDING + + if display.tft: + render_recording_screen(display, "", 0) + + elif ui_screen == UI_SCREEN_CONFIRM: + print(">>> Re-record") + current_asr_text = "" + confirm_waiting = False + ui_screen = UI_SCREEN_RECORDING + + if display.tft: + render_recording_screen(display, "", 0) + + elif ui_screen == UI_SCREEN_RESULT: + print(">>> Generate image (manual)") if ws and ws.is_connected(): try: ws.send("START_RECORDING") + is_recording = True + ui_screen = UI_SCREEN_RECORDING except: ws = None - + + elif btn_action == 3: + print(">>> Config mode") + + if is_recording and btn_action == 0: if mic.i2s: num_read = mic.readinto(read_buf) if num_read > 0: @@ -255,48 +617,73 @@ def main(): events = poller.poll(0) if events: msg = ws.recv() - image_state = process_message(msg, display, image_state, image_data_list) + image_state, event_data = process_message(msg, display, image_state, image_data_list) + + if event_data: + if event_data[0] == "asr": + current_asr_text = event_data[1] + if display.tft: + render_recording_screen(display, current_asr_text, last_audio_level) + elif event_data[0] == "status": + current_status = event_data[1] + status_text = event_data[2] if len(event_data) > 2 else "" + if display.tft: + render_result_screen(display, current_status, current_prompt, image_generation_done) + + elif event_data[0] == "prompt": + current_prompt = event_data[1] + + elif event_data[0] == "image_done": + image_generation_done = True + if display.tft: + render_result_screen(display, "COMPLETE", current_prompt, True) + + elif event_data[0] == "error": + if display.tft: + render_result_screen(display, "ERROR", current_prompt, False) + except: ws = None + + if ui_screen == UI_SCREEN_RESULT and ws and ws.is_connected(): + try: + poller = uselect.poll() + poller.register(ws.sock, uselect.POLLIN) + events = poller.poll(100) + if events: + msg = ws.recv() + if msg: + image_state, event_data = process_message(msg, display, image_state, image_data_list) + + if event_data: + if event_data[0] == "asr": + current_asr_text = event_data[1] + + elif event_data[0] == "status": + current_status = event_data[1] + status_text = event_data[2] if len(event_data) > 2 else "" + if display.tft: + render_result_screen(display, current_status, current_prompt, image_generation_done) + + elif event_data[0] == "prompt": + current_prompt = event_data[1] + if display.tft: + render_result_screen(display, current_status, current_prompt, image_generation_done) + + elif event_data[0] == "image_done": + image_generation_done = True + if display.tft: + render_result_screen(display, "COMPLETE", current_prompt, True) + + elif event_data[0] == "error": + if display.tft: + render_result_screen(display, "ERROR", current_prompt, False) + except: + pass continue - elif is_recording: - print(">>> Stop") - is_recording = False - - if display.tft: - display.init_ui() - - if ws: - try: - ws.send("STOP_RECORDING") - - # 等待更长时间以接收图片生成结果 - t_wait = time.ticks_add(time.ticks_ms(), 30000) # 等待30秒 - prev_image_state = image_state - while time.ticks_diff(t_wait, time.ticks_ms()) > 0: - poller = uselect.poll() - poller.register(ws.sock, uselect.POLLIN) - events = poller.poll(500) - if events: - msg = ws.recv() - prev_image_state = image_state - image_state = process_message(msg, display, image_state, image_data_list) - # 如果之前在接收图片,现在停止了,说明图片接收完成 - if prev_image_state == IMAGE_STATE_RECEIVING and image_state == IMAGE_STATE_IDLE: - break - else: - # 检查是否还在接收图片 - if image_state == IMAGE_STATE_IDLE: - break - except Exception as e: - print(f"Stop recording error: {e}") - ws = None - - gc.collect() - time.sleep(0.01) except Exception as e: diff --git a/websocket_server/__pycache__/server.cpython-312.pyc b/websocket_server/__pycache__/server.cpython-312.pyc index caec868772f7071675ac8ef9f3250e4f3316f83d..1c29fce3ea94642906f292402f2c823e1703ec1c 100644 GIT binary patch literal 40459 zcmcG%30zZWzA$`3b`l_jE$m^HuqmLZ;D+ojprT#qk|HN6$f74fB@;SyQPHB})GAb4 zv9&dwT8&eujk|W4aqes<06R1G{^9W5JDIJzOTFZm#96T zeGTqTir?okzjP7WI5r*1^4PM?DpJxMPfD8;$k66QU1Ak+glG-f3V+YQIQ5}+l1v0! z%h8ZQxd5K^5zWbML`w?%rIH~nYEt$x(VRxgAxwu7>1^$|XBq6TmP|6NIg1Q$&L$(8 z=a7o#95S+5Ln@ndb)xpi*xqsX_9wYG8`I^oebFnSB~g1b`-Ho5dC-`(6p~TUueoG& z^E~Kp5gF55975C*WbDfX8TWn&Q?9uLp3R5c3rLkNgpAjfZV`|PFAJI%;@^q*cM|+w zL?%ORi^)PV5bTuy}=aLzaa~V00%!K=LvWU!r z`wE?^{Us((U`S@O?}ysm5<<>-S-_58^GaPMnX^5IAgFp+DmW}(snOM)B*@%1_-s0!Jwf}iAZ1l#9+`i>fPG6I(Hqhm(i+0U zYhyQ|DV{=wG<>(XrbTb4SXb|sKCau`)ZMyWXK+XPqg8GC*6yw@U8|u%!*k0zx?4ys z*=^8u?Si7R4Z7CutzB(~w(c%>RKSnQmX40iEv?(#kpYPtbo!nS1Jn{;-`TQN*VNl( z*rwCB=^-VwR@bGYT5vs)RTWKjO_h!7s z#)Jeh@%qc-)_(8sMb9boxb@KZ)qUPm2RuI;9KU*X^47?>^#jk@bK};lU!0%5%yxe)X_t|5cCWl4oF_ z=i(Lbz|i>h*F8rrj$5yJjvVz|x$HS;_6!YrK7MEX%IV3MZj4)BnY^{%^TE*M#mnPY z&rO^=H1)`;=p?ohb^89Z+Z@nPVGN3g?e&!{Kl)EfseeWu6k~~I)3BU#EVzPuN@w@ zzV1DB-gEex_uxTq|2fY)H#}F5d9L?+F1+L!8koE_>^V6)aqN<3??>ZTPfoo2k>|i6 z&)MUiv+sG2-SV6~={@nz)QfLVoV_`I<-qvWS3Flf^xSxB;-uBH*F1T~Jayu-cl6_N z>tWB{4?ROKPwhYCJ#Lw}ddstKulF^J_k*`Rw_frbIW&G_)H{55;?ygi!>2t*2F9-p zcn03{UOeUM=jMR-rM;8=d%dUnC*FT${My^zmp=4- z{EMmn*F9H0@*WwQIC**MtzpmJ*C))ccrLv*e)X95%~RgvBa@fTc#aNFz46}ojYFQj z?|7_M@A3WK{%hXzXFRXI;JJRpd->Yrg`1uu2PZC^nK%H0`O(yCx4f5Loa}$WbM~_5 z^wr5o=cY|uATNCerMwJJCiqG@>upwTzh%)+zXQz z&Ugmi_q=n(d*GGHb3gN*I_(|3GI;^k>X2vODevHZ&;HTzD{puPuX?Uu^&Y$F+4te( zJGUm^df#*97oHbh_uSg+9e}qO@SJ?pGcYoK4Ho2a@1YBxvoB8#pY|MrMKv;c;1`~~ z2c}M#J%>IVzx9Uq7uP(4C#L#GCq`~go_l%Xr4Odw+Ba@}$qP&9$lK%AQ=W_0JtJn% z;a5DjPK;ZRPwhYIdF##bs|UQlJUli0k@pu@y!~%@PhIrB`NH^Bn2{5cFB}`c)<1dS ztY_~Bp1qg6Z@lC^_R-|U7re)=c&&RqA77fddDye>(A56@lQ+(LPhR#MdTr|M!=8ct z-UF{rjNI_PHZs+Jdh*~Uj}_MW0ne$clULuLyz%b%%~vK)o%S3&4`iI(gxO=WUDk;5**qCp;GhCoWy{?0tXY-D{phhdo1wCeB>& z9yl~{?x^Sb==iNOQ}3OhJow_|#WyA{edvAd^2B>rJ$qmB?EB^TwS!Y9_j#@Fjb9)2 zoH{*u!Rr0sor#-gJy&i`oc?g~f@SJdzxTo~Cq|D=yf)~4<$(9i6P}x|c#gb3Zap^f z-ba(?UYz{s;Ka54lW*_!+_*Gx;l%ij!`{IcrcQr2e*MJwm3KYXmpo?=c`jV>ynJiw z%(bbJy`II&7O0|CN7V95B7V` z?)Mxz=sEc2)EhsWIQ4>O04f~yyn1Nj>bu@!7bgeKdyf3V`!bBuhvPS4W4`6N**~?{ z>N&j6V|~%H_m%M*uT71d^?o$qee-3{)l(DaES|GJ_w3&{@r#?D{_CD2A5R?K?>%v3 z;@V};z|W@o4|qrSd-jcZJ{b1&k9aSh9KZSt&#m{puMK#QUz{3w%X|7A@1@h8H(_3` zPaJ;5I|%u&d-h)Q4!=5as-A&!sm#r}lXU_fEX<{`id}la`mf*Uoqj-|}4QpV)iObL7Q|t8YzQ+vmCdnr8@# zyfgJGfMuBdccyNROuhYPs}C*0M;k)>&~g3MB1qlOKS3C9G78##=z%{0rNP-S`{Ga} zJ+GH%5VOzQC2Wklvqy6u&mdz{O>msEsK^51Am0$iJ{jO|;gS58C8U6TUPU|;1*lnw ziPs*%=Hl+A5bpRMj1dr0Kw{B9wlt4`a*VU^O2#ch_9YSqxW!pn{H0bau#8=r6yv930 z93a{QCk9f{1)cY$HW8YThABb;s0Gh0pmZ%H+<9B{=yCspnz@_wI!ZscT}SCUd$+c9 z&8^buw;Q^5%-zvz*w)>pC3U;z>UOm3&{4&6ck4FmF*>4!GP`utT;^7|qt`82($Uhn znQU2ROl@uHXzl4}G3d0N+iUj`8-LoT$dM^p7}KQWqiF8fTjIr?n=SCBwZ zF%W2^?vQT1TdS%}e<4r*E1!s6@C}~`O_>%E@@S_l*(OW2%Tj4U>NnzG zsSosWX#7q{_DUZA6P|Eoym-bU@bA6^SPuV{z4s%#)xQ^^6TZvf-<9mbV()!Fw)g!z zmaU5*OoRcwAimgL278-L3Jybp?Ogvd*Ju4m>7czJc>(=ETMMp#t{>FT41P#q^7pPq zYCvlnUu1{@Ekb;qB;E*sJyOEpCpb+2!Pt>mfFM^$dpui;yR#6eC5#lHe{2aJ5zwB{ zkj$1aB(O1d_JDj51(GiYHJbxqCT!C_fgyuUF$vgGJYq0H5ZHz!pbfNn@WtkaHl%@V z^a^?fvmxf-lMGDEAPgyN`AmXJ=;H^xiP*%SIbx)sCG1IH0rc^o=MO&1+;{VK6F&GF zis7qg;0<&t4j%V>0NBB+6W3owXY9`g0nk1ODJ(RG)^v9n)bOiJO@%|-4aSr;Kvn5T z^%nF1!4h>_m!VtTsq5^fddt)gMC#4G2A$p*UrkZnlp4`sZCzWr>}6_W=sHS=(xDb( zf_huq)@=n_DBaE;T~}+bx^-I%)zS*!ORtHdVxdV2&AVGnwozRzojSL;WwV|EUs^3{ zUaNHr+PmAj+|ruHhK<^q`Zd**3Kc`J3ZNZylYwzNDCj7K4iyTmLbn1x0sz^f-Q2Tf z3&2Z$2Y^m#^`2JU4u-g^tFNuo)@-O=uiey8U)fky?GD?jGidz<-Jv+z+S0mBrxChC z^{rIf4uclD>XzassG}IHx_LUctf#B3wVTvwcLC5xhdzK=wVHvm5kM*WD#o=@3*mVo z{OQx+FzP4177@zm6LCl5PNW`9J*zwycP8#!>Y3EbDtlU)J+i!i)tEwgLVQ$wLVi>} z+GI~ ze?yyVwnZHkxk`@h(~kB@uH@zYGGvJOq(L>urMA{<Aup{~v zg48Dn9ns7qiRKWJ*UTqFbOMqOe*&Fwi-;5g_fFI+g13*tEFz98w6R6Moq?_=Zvxo? z3>(Cl0SfpaAhTzHDFWo6TWo+_+BVWHY0*As&{UlO{Mkfk zA;w&$5cuU=5P&Fpbb8I$_{I3Ds&lyU8AWxX88Iojolbz9(WW zFce!M3m+CYg+`NVg5W&{KgY0~szIqJK!8J^3c<_7mx8S65Q#8hls`?t&8){aYwy?S z7w!R53=Jh7)K>p2LJFFTm*DTK{|jbpKe1o5U%VP9DG(_H;DxY#Lpac3@Xu{9Aj0BC zB;N@6SbzffgW#Z;0|=zR2ax=0;ego0J3z2(+JmA4A`bUGAYKApf1gkY?@3Aw{?Q<%284&;Yl~n&G$0-zw(!Z&m*ssS zK%2@Ah_rre{1NTR+4uFgBw$-IAfg9f?a^$EyOUx6P)UTe%(@H+Bf%H!-Vk>DX1CwR ze?*VNXX19V%K=Tzhxhi6EgA6-jDq4HdYAvRqmbku1^y3=g1{tz{`)B!lYrXEVczZO zY#rRaJ&TL8F%BQ6Hjt73z<4PCTgGD!+n)mj^vJi0AD+Li6=YOkAIWI=BQ5avu<+Bk5b!t3pv666$QCWPJUCY6 zLk0Jt`gcL_Z9j3}hkG%Y*mQ_UT0AC?a?0~<@_d&f-l@p2DKbX8ERS32t?R9OtnGJs zcjw%Vr!$-Eij8B6IHw}prpUG{a{jlF9~S=Y*Ah(p@lY*5{NR(g6vAQg9 zr-Rf8s0O!aOA9bLNWAuDJU)AA^71=9h#yqHwxYILTfJdJTBpxd5p;z z$DR!j)EHkcQ}?KUsFnR!r;d$4rXOwx=hDQw{J7X92Ansb5|*6lW8}j|-%< zyx6kPqM<{VI0Q@YhekO=lWn2NqY`tRSxToA+e1s7f)ZL#GA#%d7N4!0Cg65C;~R9F zR?QJ+yJ8VZCA%LU=L}D?g{RT!4fgOxr=*dVG>(PG9O*pJIb3NES34zYTB06L%WvMX0R<*RA=>M>>fiGrgAqkOwE(<#rS z<(cEk?9pCJu3fp*DPL-nFTJ0Tj!VZEz-?7PB{g}`t;o7Bi|AiH-OVF}^S_%;C#36m-?=2}@9rl)_uYNT^6y~srBC>h zVp_hPmJ?xXc;C9@ncv=*FZ~vthSi~;b>EH4qW?w+h1EP)ETV#CV==1X$X6eOXee;; zXa&PK3omj=in%e(top3!Zsezr!ThAJwTCx2B@H%71NS@~X0ysEsrr+o>icOKs{Q-v z@ELDlQ)B@zPoD7~`Z755_g2hZUBJ6DpI5g;aA$!E;-7>k)+ZC6Xn1wY1)t<5LHtt@ zuRcWZsW=_QHN1ME;L}``=QAO%ULyERl#Sx~yn317vjUX=b1APrT=01)%Ktf}M+!bK zs1U=CU(M&$M+ttl0A>1hB(FY3@M~ohif8fa;{?CXo&)i_VZ8cy!QF5bif8lc69sqY z2v9sv37xyUoCopWgy0aGhqwfXG8~5E5Nd|>C>+M%Fb;?DI84N0vUGI}|2Nqc!qp1? zzvS^&6^i~PKOW-074lX~M86fG^xw*at5w8r6++1R+ZaBItCXvA`M=HOLszJsu<2*u zS?I*ZB>%5qU>RcH;K!>#0OA+G`UD_=>CL`aY^V=7SLh#y6>(S;$>)U2KlN*BA--Us zP{Kq6#nXgbSyNzHk-?9f0e0mVi~!mkGK;Vmg9wEqTP4Bj1G35YZXk5Y0HKQzm|Q~m zCosYbClGxi=$)T%;Ksn@#~B}PcQ8u;cx%vilCqfQ!=(a7EtJiLG}Rsw*H{>I9!MTN z{4It*HAd}a__zketSQjQ!AL#QIs%=T0Z@&4%RBJ##QPUnDNB@TEwaW-Od?z82 zsklS2Oj(Qi>*lplX^=^e_(=FP^oc#F9jGV@ZlcHk1_3_v#zAo0KAh>0rj8c5Vq#A` zcJ#4VpBM;5Qerho1cafqJokKw+4%l)I=9?fN{6m=2r8#Vge=;3rpeDYSrXpY()mlR z>#rNF9dycSd*~XcU=1x;1NoG(ATSA24iz119nO8Vd(`-+u;KxpOOY^KG`xP;IPx^D z$Q=-3K$va|Paj=i57#&)8keS8z26dIuvDKbKdJ^}m|8v)??e#3SrCEK6IwI4MJ{qa{&Ix0a*5hoKpLB9}a zAVX0~pNJ#>r~;D+QsEibd+sT_rMD;GR-Waam_%$j05@V&0Eg5>AUfM5ZX!6YWINh} zv#xeOUy*z8BPC+EoNUbC=Pa^)W$%5GAE_;feB2}nka+uMqc4EF8uUH<(sH&>@b-R? z33>+MY@n(&@d=pX3aGV*^$(Q5KAC|n)~ZAheu{K1Kd^Y1d})W2T`)vI?I9f5zpe!wl- zx*2IYPoc$Q3|Ur(51uJR^GuDR;O`J%_R|z1+osg$XC?~JDrwLtDMUrP`K{YPRS9JO zAR4F8*@Hw$R8Ok`^q0DJfL=CLfXZ;BS~mKL7^@UO8v=_IsazC8tN?tNApw(aI~Mb6 zWE6Tsw*cuCxFuURYw_6yzo9v|pamE$Zed#&soUcgb#K|C2YqrzbdL@(ssz9p!RSq1t>6w0Y1LenYm*PH7y_EWy%3id}p0;{u)tEZ{ocN6RocxU3+MjV>189!%o<1H{ISgJbG2t`&#gJL#*$#&_}OB6=GtND!?_BErQ>n& zCw3p*ed76}&yTh_;^vJdr;TdN8y(5TmKIl1^68q9n$glz4Xzx`MbUdAGxdJxaQ#?n z`sm`5&s&uDQcGNAm1jfE&~*PMCZ$cU5@TXHd+z3Ta(O_oyfR|O$gd=m+=X%qwX z0iH0-C6T*i5f1=aBZya5X4J`uPZm}#g4?g=me(%l|2kI+@n0{Hpm4dcP9nZ5;6wbb z1c$Pax(I%&4{$*Z1c7z{`u){D*cl+^lI!CxR(_n*3lbpfuVwvo3ZFI1pyyw+A`wxOe3dU6Ce1yI=i_tI*^hB~ZG07jSw3 zRc#6}B(XWR!}wE6ND1Qdg5b~(%G*=e9NeAl7ajq*Nni@`p$h*{DN=TdZ&0(fndAs9 z2v835kF`m#B?PI&asol%Nf-3V3LeslzYcOB9vdtL@&ntUb} zz`d6DASmiRa0SdSd=v=Q_zHyVU=J~Q@iko1fSBs?(_yhp^8zF+YdY~iFBdP*=qfvqw9e%Yz z979mI1axOw^sQ}epw+6|)6ur2*DWH!YzIyTbc9e^6g-E576{z@7Ls&}T6Q4a1dbi< z4#!>HP`z=(liIZvPhbY4TZt92u$r0P03v-|4|CdR!WjJpV9kPRRtKomx@Ao7F_Y3A zrtfJ*F9_^;)O|U_kM;eM;6g|h039;aVH6xkJ(EE9wU`c)fbzqR?k-RVM!Z3{7P9Bha84(A%wbOz~Y3^7;^6<8kNa;$IHM-rdDjul+bA;+&6{)Vo3}<4VEiunr zZ%-_BCN8xlF12p8C)Nx}T*-6Hl2M~2(z3{sOD8TJl3=F%Jey*kD>~U3J;xRe8SK$T zW6_Du=v-TLu01;cYau^UiMW(0PDPqckv5w5g`)7jBFV??uX890zMak@Vp9G(kB~$U z%g%P)Q_XipCk&Sl7mV&?Ihy7$T3I?Ez8{n3icyai9o3G-C!dZVi9eb6SFvDDs6&!4 zywE1e_*zcLBL@25m>f3IvOHRl_wCn-J{7TCEG5V_FEZy^R$6viT4`mOLtgHZtDN#w zn>=+?;*@Ld$u(GKESHfS9ABAI6&mtcsI;08eioHpRWAIjTnO!9vswsdP@C7^%Kk)=oF8&M-2XMK8!x1uz!-2RR0LuVj{4v4` z)Ib@|2qgjZg zwt+wmG}FX8T6&S{sUF6L;I<#bn~&r69^Q3ZsFtmrNCNNyWS#?auRjG5hC`u92GU=V z+h6~sEZP+j4QP@wX6VURQ-HA$sqSAjj<^d$?W4JW3M)XIh4|sL;fmq*ku|hJ1DpkA z)R6J$+yOBvraF;$G!c-uT64R#%vM-^muD-izuRsrc)}j}bRy;zWKTj5;qR zOd-Hq6b0KQq@TA4JJQd41U02d_N_SF3t=9Cyi`ub$OMc777KbB?QRJWt6XiNCJB_4 z1=Q&$v>{y%nwq|u0^A_@IE3LE!h_c^gD=a>BA-=*GD)dCPPM!}mVGPkPDZe;`$yMb zdd3?{NyYg{7XARY6xK)3k&kBUqtXBm_G3c<5LCvg(Zk5e*pYj<{YSRpSAWqfAJL0A zwim#}x(q8IcwhbX^vPk3gqh@&Dxk-HB{9J2{J10Z*Ed^z;X}NxrGfcL75grI5vB+- z9<#v`FdIw(zY{SVjQJgDQjiKC8!X-w>5oUDbj$`z!fY^j;%9>;V>XzQ!%o}Nxp`z` z!BnVzq{JMyEO$2`f*fD$+p_n*s2`~<7+i?r)P?*b6#XOh6tF$u?k4}*0tS_uO%FN> zL3=@_1oXpqw)g#0{h$^ZkUSY*rf4#ieLG+)#h7B+(TORR1K*HZxIQ7v8LE|MDxzNOyO)z?TcqUf}dm>`?N2P zOgDuAEDATpVX8?L91fXOY>8ldwU^(^2emQ&pxS5eFW8M#%|Zshz+oK@^Jh^^#XzHS zkWy(14Lp?|2@j)Y^aJp*<5=;NQThVviQgCcd}uF>9J+I~6YfF5tRARl^dM$-4<23H zY{uN+Z~W4t13%bC#y{Q$#?LYtosqLIgTgt(-2+t=Fb8+bYpcOd1{uqMU5}A5n?V*n z4el`X#BcO6MZ$EudHD|%Y=I{2%8HGZb!Eo5m54S`*B~394ajmw54J#H?PC;S`yIVe zjvrwP1{wUQeMD5g+9*~t6qJ#{_Hcz-P@!R{%B{?TyT6>F8hO;4kYLPUNX>06U1SF$ zG;zI*Ns*7B1H&UFekHEHd1dn+DA}Ha6;DO&+UkalWya*0x><P;yoiEK6d;ZX#&fA56gvt5+ot~oaHIaNYm%wwP}V#Y z%tL_&1xV`!$Q_=@f>=hIYqTmn;W^!bi&c1~n zybXaK2^cu6epZY@@}ux1=olj|-xN@vLb+vdKK|$`{Jmf=kJNLIBk{s^V`hL zbowHD_+qDIF)dl_l7!!v#tf(4lje-a=bGn(a$5YnfpC{H=0wiXoc9{+iRJG#jP5*D zIm&yZ#@t{_EVnC{42U`X3Shm0Cfp457hI)Nsn(naixbcQy9RjW&7Oz}n$b$TzfRGlt#bMZRc;&t@;r@vVI%(Or)UFeF> za>mcI#m}RQs_gOA1FKwdi34?ij))&iNgK^M**PQ|OVXJ0>`A3R1I}^6@S@QT_PCrO z0SwqEZ$vt}(H5tHU-8MKf{{eCz^2L{5(4LaxaifzqcP^Rw-dmMY_w%$kGXiH+Y)1q zycBOsDYsI#k)WqU>4T^d=r5E&Kj&V6C^rh18Fdh_J_bNDC=a^8jd7#uRS} zUV96zJd2)ImOJE20(70q?8qLmAr@={t}it`HL*@JK9G4J_cT17<8|L##`Z1Pg9|GDr#^x(*fcrmRpkM}lj zEnjdp9b$Jx%C#cN9aRd7m-5!i1a}t3Lj05PibZRqiO)p5wc&!##3?AA#akOG_$*tA z;)|4!>2p~^Lk#hG7Oz1m_&i&H;<=fS{;PT({|_nQ4MqGvLOad+bQ7moD;r~$)-k8Dv zV+zXo#|$A#$w4_=0i^jamiP44I!Fq%YSV#kkWLUxNDuz1_{hSI3?)Ty7sFi)cL^!c zfxLhe!gDG74(%0cWbT-?y#ccL=nG6oa=34A^XeAJB8qj+sgD0J#C|wdeW0zB@;}XoL-54FJ}q zMmd_x4cHPB&|CrhDl=x)VC{0G6^3}9YLtaB)XeR?Ez!~Uuvy71_Qkd z-3yS))T5uzp#bekjEian%w|M0OW9Dc0D{?up0F~oQNs_c9Q^zgS~h~!4BMJ~xlyacpNfgAOAPNPa zv%wG=A|!fznV1gY3zJ7R(*)cs3!NovY$a>HLO;F={oMcb=j@PH}ZC+HHxZ!v0- z3IY2q5dH?+#{oSFvVWl`ehWXg9S}?RfLIztz8XmbqaK5292y3VnI+q(K8Upa9calm zqW|xI*GSw_q@L13Q7yfU^aCA{j0gnWM<5{x&|BS|n94&QfM7q8lo|4X`w1DNap}`< z+1dhb2qJvaS#VguxcEWC1o|~#Y~P`q?SmrLmzl|`Eh$De5Jjcbzd@Owqm}hKMBrqJ zO?`EHeqw_}!fT$RF3`l~=xZ#+eg_~o}V5vvx&*qvz*#pwU zlbvB1wy=z|HRo2JSq+Dc7h%h$6N&IVKvLz2w1Baqp0Q*R(8f#lS(w;BJq(0Z z%mkn&9wiNdBL?)O1Ndgh1`Kh5eb0oz_xBwS;6%oMkOVWZCdF9iK!R=9q;CiSdy<`( zzmFy#F0}C5(eXCx@~M7315bk-IKhq^G6WrK?ebX_p|l`NG??B7dw_z}1?HH!$S@OF z;Bj&u;HdE^1sOqg(^U@K&mB=|lA+c)q$I(hM3aB&BD{?h*fa)~!Iz*f(gw4u_k zTtZGKSvxssAru^x0#otdufHBzRHz0ojuWqqct@?sk>LcjO&3*e!XtqHwO=mO}K^$_YMZWer;j^7wT{yFy_ z1nWF-wh2bFOdZVFfge=d8J{WO6vFX()DSRJ17@GlbVdEclVc3sxm(v&VCbPXclRJY z+0amC>>nO-7_FWJoFizP@OTPgS3VoWspa4@LkJEjbRBLHvuxaD@Ae zt`mIFf%gqcXH@!@82DCVvX-f*khW6~qG@?nJ;lh!fo(q})1 zz1;dZwtzMA)dlLHHDOaj;Pm>uKKV)`UM<^;pv8Q)gBm%5QXvJ5cHm`Yy%+G$0yQ1zuO=}UR?@Mb!_$|0{Fk4iqV{OEF*GQ|~}F{Gt4 zD{t4`E~D4B(NXQLxCECf@oT9tI&?sS46MKjUaC!&I$Cm1mJRMq5ZBMCnhPqK7M@L2 z>{ON6RAo-pN}Fn>Q&neE)!9|6d<;P_Qybk*D~lcSl0QeNzXWBRbUJ7GZRy>NyT!M| z>E(@g^>lWVL$UFGR1z&u!v31(SyUe(DsiCkTWokH5UQ}1nZ}%Ej-ta09Fjuc>$lJuW!For zJFk^mbLg~6hrG%a5qBu!`)MhZd7#gP;eBIeOl2|gNy#z@KP{G4oRFOWqXplwz7Jymm18|n(K6`a4LId_zau1sD+WERMM%wIh)>1B^I*p{ zTFtXA&Vf28FCZ%hs6mfnQWT^99&lXw)hY2o#ULj#P6HjhqJ)v*9U@Rs_{UrK3+?;F zaF9vJFi-=Mnj{$Sy#q&~C>YW}MsnCJ=)gxJpD>`Pz@a~WI+c4_PW=Jv$p;&qAgBSU z#G?S>ykO&L5^&y&y4r*135IZxQMlICoI*qJvSc)i%D{{U?Lja_h%O!(1A=}I?TN*T zn|>7;JbQ8$&G0?*BNe^j{_*Dx-uX|=q+e0RH)e&w%aSUD9+F`=j7MRgJfN=$Ciz2X z9<$OC$s~3(d@D^3iVk7z=+v5ZwWG6c*7Z|4=c$mZC!pnI?)l6f7}D6P{D^5dUg?oY zIU=Z<0y^IC1%hk;$Og2PVhW$xh999%CDmLB@E#OKrgcPhM)xUT))eh|Y`xr_>ovP4 z`XWt{?FC#8HfBJC8@{;MmJq0Q9-GeH**^3|8cNty|Jy4K=ybvtR}} zJRoiBi-vv%6ZS`&qPF-~D;Z9vClF7=-tLR(i|vau#gOS(9VG)Pq4cRtv9SLE>XMoG zcf1Ky00N#Tm{eqz5lWll$!v5t0@uKlfWp2+lQNh(fuWpzyLQC5n01kJOo{C1u;oU0{uweXV+)cwY<^ItNHQh0SFlgGd!RA}yfOS^%K%x&?PLvCD-bQYhDx>u zt~{jY%}~T*NyU7!AfQfv@0ySr_bRp??hf@7!xJQ|opr&=<^KqL0Tt5iu_g295Vw#loz>4MA52>KP|ii16F?Y`wQPrg&jP-ym|DTy*LNbr8}X3^RJL_@N%KGqxh2pJnXW{^*RA zKa}%7HDj2J^}Pp-E=vM0`SGy;@)wuug&l*&fqQ?=(w=D;NrpHApZM^_1)vjwYy|QY zzKap)5+xL2*f^-5w(84p3?;F20z=R;CH(YCU@q)j(C@dVO#QG&9(4{>hO^~^Us~*g zP~)#5K)1h44PTys4@mgz9?qWuV+emfL|~!3Z909K8ot#4MEyhgjLEgWPk{iT!&qt~ zEg1GyrZQkmFe&Jx9kKpR#f;%VG*{&AmaaX;h4Tsw#cd4z2n^%DKv`-W1^+h$K;$8J zkqlAf7Ow+^8Spsc7H`#YVkcLH9%&+!bjH-(NXOn+9? zGaU<@ANUXn6;&_RD$}=sr&lx-#%Lx1yzqk9gH4@6Qi>kLRQK^< z7{w`UrVq7%qf#L*Mj@~@g%mrdmZLz10$?p8er}yRWcOxc)JEh6whJUHT3E$ z<&tx+psyfYKcZpXQNg-oO?BGP49%Di_%H`j6!TH!)D(K7unovo2bzTeQ0HKkEH`hi zo2ThPD|<$}cpibF!VDg>Q`9*|fdXt3wIhI*KxHY_Qe2WBk z>Y>UXUQ8@IVGhGiK&%_;|3Sh3M8W@pz|99i7UOYfJNhZyq60-X>T487j`Uj$ps>`4 zeucw21h4vqJ{2LX*@X1mz>PE2!2pI`I(Kw!bwl-rijCD(TE^c3_CLK6RUp!D18-g( z$kG%(0>FnkrTz}-jDae0i3*+(s2<$#nu>-KdKV9%TNdcW7d}r0 zWGgH{RuRAPohiol%hiqN|(srB=Tj4U? z8gwk9W8={m!^J^wOrhZag_iaI0ayzdPARbq2@n^WRdi^vLr~)5XY9bANk~6EcVw|<00P8>B?GJPM0)mRhhlxl~n=F7zVFOv$) zo9X#=cjNEM=;Dozq)i}ZosK8slEI`ob`A(-6sjXXJ@8X!M7k{^eRQckqR8^py@(aA zq|DQeBaP;ITT-cIr!8r*HOrQ?Vn~VxNmXvGx0chfwGKtypZ#OC$`QT#FY+{(TzRD7 zK!a1BZIc6=%TZk8kk=yZwnj(hB06I+W9$V&rNmr&Lf*jYf0f6hBC8LqcBt~r+a2;U ztay}TPQ<2W4*5bqhb()kL$PdHlpmJ=KNMN6*u)d9obx1-F)7S!$21AL4`kU24wB?L)Hh_>@z|KgH(`Rg5L4pDp`Sa^Wz~m7aAj|4hDx zXDKr8vcoJIIs5TYM+(x#T1tZ$w5LXI#Itls%GG=6%L(b%wDX`|= zZA)8dO}C{j85X%z8BSHMO_h7T^kVsYUSA;x7$}W)Adi$Pqomo zn;nW)-!!z+c`F>z72|Pvv?6ahfq+R|IHb_pu=h%x&hU zXysxUbm+e#aTsPE6b564!$!ATD$aD%S*4a1tIX1OyMm6baVTp2J0ri5UiTFJtk$+p z3vRsV=UV8^opehV-9^#4dPlTjEG}zw3thPUcH-TgcT(vM&)DOhr4`TmS4;ki+wr$$ zbnY5Q^jgpr{HBrz?@7lj_#TA#(W!qU1Y!AjJ61RpD}9Z^yEQnX8^_}E?kV!dqLTcX zRA78>;nT769f}292UmO+b$k7%v9~33ZoMOV6%_kIk%#!1Vfo*GokPfD{svNXh8>z? zjx_V>@O+1)0ER&xH=KA6obWS#?6+Gh-s`3dmRVbF%dCBOD;SkbAHNltw$-<{(;GM4 z>87h6qaS;mexj9roTSNZbatCV(GH+#G#k0TdFpP_6)j9!RPz_|X8(H5S#rJUcI36k ztaWsDwL?)e78UpXbUgIxf&MT+!jsj4^?c%UwGf5bE0@FVZgyo(V-oSZaAl){|2y?U zi2q(ykpoZu$O}iILf9BD{$q?79{e#WYdyiIc|1s``4SYD<*qB|(+hYgUWzKG%Y_iP z5qyZ-_|hhb%PYb*l=JV!^Wew5)HoC`2SFO&D4n}oKNmaTpP{9M@T!;K@2gY5DfF4C z1vPVeA2Oi}j0=dF#%LUmGEn~tG-bf0J7@wSqzzwe4Pc}RoC@X&#)0(b6G(vx4T51J zm?;yR%ikZLcWLl;3ugAah#>(L4IE#_?~OR1k3!Qdf7vE6=qMn*@Dl;K{G30am>+tB z7RkeDeSF||BUBS#K*jsD6krSxzKbtXLQ2U{Q^tf#z+UK*6fG_Ri zoA7`m-t`UiJRJ2L%&7iHK&}YR?1|HEVoN?mrKey6tRURQpgr*Wvj%=8%&-aEA_TOo zjuZS`G#D9#ALGlA$;P;k zgZjM3%weB!cdl>fEzm!X=K=gl6&X(^kieTIlgShkxR+e*oaxn88Ssm^nMotlgK;uB zgS46B9MPy9{7u@Y(X$O=%39*f)li0uE&UvAJWl>iloy z|5`%V#YeQcknQb%Lz_L1Xmb(Uratl!ITo`ysAWI??tRJ`qopJ>(Mk+BMf?Vti$KQLx7GwK!pVBRKj9l{u>M!*-=;hm7goty1D+1cdorX=}vtaXev?}T_Oj1PaX0s8MhJA<7%FlIYu z^kL5IJ{T5rquE}@#<{z}FH2+R8}JxWu$=9Pi8xO6y7#s#wny>Yenk-#_Nq4&XQ)%&O;*;(AE{UE%y5gc=TV#;Q`ZDYQfJp~wEx?i`Y_es~mu_JXVk=!Xx|{FD6%xs<0~|;OXjs9lqwIfLv>ef>s+$(Rl6xsQdtk_DdF` z{QyL4oDG!%%74L@XCdND2uIC?t?PfFrUyZ4+QrTfK=iyB5PbtX8yKfZ{BgP-Jd}b3 z6oUUK#*8eA&3NynGoW$P?B0TGJLAV&>XI|*iD&;bR{4!?)n9sup6B#E4tt~xUw;43pxXKs-CYbnhmTF}01zz|fuxD`XhU&`3 z4M<#UR5l@J0;q*<3P}@@Y%X>Od~X$`#_*ZO0&s@ffhsT9RqW>PXzgt4Uk36b0hUF> zmq;K}3&_71>k%y|9K$yr{ojv?se-RbH-W56hpotbCXL3J01GqRII=rayTh9{Hm(ck zSy~f(S396%>W#>!mm0pv$$U;0M1>DE&<%e7NMp3modcBLfdr(ueq-!9M%D=0x!J=w=tuN z#d-k}ePd!1{>(Yjbzo)6V1?VJ-=-rAjo4ub{wk91!!wL?dW5JN<$>uv*^pEQ-yTQW z17-8dn2%(FzCQX2@hLD|IPUrQ7tEJ%s1G5lTf(~Hpf18s%7TJlqTnMGd<=mx8oM6x ziEY_W*ie_z^XF0UQxx>0-~|-ChyuiKxsk9T+O8ONuqHBe2I2^yOp(An;s4L&^1B z@Lk|GQeC?eIaP)gMC#Nq#pwIAXc;|Wl&EtmV246pX}-E1xhzJ-aQgr~e6CTD6BO7- z*d+@T6Jl#HV>*+IRU%-v1UNnE)S>0ZY?-Ja;KnHSy`E+X_}K(Mm^*y#N3f*&Sr`Df z0$GN_=c~}N$KRzwl5z}(-10zJ#kez(QXw8_k7ViOkw{qz|y*ae+_&fa6o> zIx>ny`bU= zB!?jRK72=FwJRd3ziuo}{pwF>LG+kR>6B&IWEpl@R)5vBpouSxafK_vFeFUnQfr** zBAdF%t}byVlnjZ%)I$+F9Ct554WF(Z-gWe+LjoitjW7#FchRwh4#nL2QE_x){=KOD zvFLa@q0kXMcPu0S?ed{DV{_)c{S!JGOnAd$a>gtb4*)!^gC1cq|uJkfjMvg0| zz?D@1MkR9+hGp;t3XuG(G&YsSp~^$j{FDWMQk6n-YyzEJV2>$44J0kS-uhX-eOV*D zbRC_r-Vwb4T)(I?rb7hr%UnrwoJsk%qj;er< ze_MWPFR7)A>+Xg-9^Xtq)=IbP=!7kf=&g`Xojz3eWn8K&BhQ&pYRf2v4kOh9FhQ|| z(kbN*)snA8L_&%qWu7I|0SXF5@ZsLm;t}y^ghMqCWY!6brX^`HD|k>F2i4-93))aQ}m+yV8^aKs-?3QI${>NQqoVi zkF8)-`^ai%P`r7X6mln>Ry@;jSviz^&U%`?~9Qs)l~Tq$#WEw8y92j)YO zcjy1H{5R!y3+Tt6pr3rk`Q#SclUwK~w$fX-(QTd1wjNts54~*{UA)_#vd5{~b5FJB z{>&=q`fj>-hdpJdQ?>J+YUhmJS*va}UTM6$%CY2e`YA2ly7S&sJL#U@X@Yp3w>IP} zg4hMOX+b#{SAl!`)5}Me!&k!`iOa^)7g*YEN8dL5+IYuEZ`cH#v)4XJH$P>oecGP> zj5FyOThcSGeb9zt-N-(wm-hRJXV?bIz?gv&yWuRM<01 zhpU;|Vs7ueqjEgnOh5IU{qYu>)ZKercXt83e#5^kqPKV2)4QBWUACmIabIUCd-^J8 z(keP>)wk2zc!V;3xYib#aUUSSZd=?O1Oiyk$yE-82G(hEiDmxvM0-;8kkqA6I~8+m ziaBPfLs9(as1&gBkE!EfRgn2M)qKm74%PC%R*Hy-sOcy|nEV}h(Mg>DU@Qu2KB!z# zPdJ{|f>MS3>CLvN%^0XuJEChK;);zM5@Rg})%Rb|Cz7)MM(_c$pv}3!(qd_%Qx-c^ zWv=YpVVNsq&KnYN?E}xh|2lzCYrr)^{4xX-G8b7Rt$aFti8E>Gy`-hcGeZ2b2j8e+ z+k&>ygXv7zLf^p-%B|)-07uSA+0Mj8w!}rCa1pZ?<-<%Ob2d3J>Rn4Qc0&Qa;O&j_YPfL zV^67ds%mYjT6o>GEHm$n)V$G_S~M&egI|I(i57uPJ%3nuA3oYpbaJsd#*+4af@@Bm zx#i3rOYxa*Ys~G)EAh7M%G;DJtA4l!YRS#N824V>#nkswuPblGU5UGudL{LDD*NIM zc1_dpsj~?vdT6pC5VN+~!DKG^Wm?bFiY;?RI+o=}E>vu4Tt1;c92j>U%k>$8!oSa?p8+zxFJ>WDUKhi!SVTucXQ~b$LS65Dxh$*m2TNa zZ)>A7+8xQ;|NfnC;Z!mcz0}%bh22mA_LuimEAhgqxAX6kb!b;7+Ol#WR*b`X}nrjbh@{*er;Dx+HPq zT;j91|G%fJ`EB9~;_vNyYkU2*yN-hsLX4dl93vBl6oZ>Uj6+L_%Dr_V;Gq zhxcs`JF~Mh2^|{udR$32yEk9~{yR$yCWGYu5Ka2|{kRACgIzQk;14_r8#Mmt3PP`k z3QcwMht?M4oiw$dKlBd!pz&ykrb7Ht+zb3MD8l@)_ch>ux(=rX$rBSzMfel*>%g;w zruuo7CiX%j=cB18&-q)CAEBuj&mHB+2NM#EembO2hDlyUp4TZ{Y2HoYy7C@noU^>I zZ7k06F^<|}LVH|7Nn8ePfisX7%*uF}6*Sj)kQKUQlmumzz}Kk2BT7=_9>BR(*qsQn8E z6wtUe{O#$lPiMV{^6q{^DG+_rUdbM9!C=+N7#8P<}`VOSuSWtM0)ZlS)KyPb~EmoY*$m>H9Hwtscqu$Njx zS>kG<(`DF7o^Y19n+VHVH_XcNwWVvzH#3SyubNI$s{I1lr|^7tp)StVw(r|5@x~W#{eTUMcucoCo3c0EFL|zdj^}{;tsP}|vTm38@ zm1tY_Lrx@lxpS%W=Jh=7s7fEf6WoeYBY3VeBJF92sy9r8+ixyDW$hKNmsJ1FtE;L% zr~311w=q&uyRqG!Qz55AUOi}xtnDOtSUKA)lnmSPme>wU2i9? OM?_`y--gsv?fgGBhUA3+ delta 10597 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 diff --git a/websocket_server/server.py b/websocket_server/server.py index fb5d663..4cab5c0 100644 --- a/websocket_server/server.py +++ b/websocket_server/server.py @@ -31,6 +31,7 @@ HIGH_FREQ_UNICODE = [ord(c) for c in HIGH_FREQ_CHARS] # 字体缓存 font_cache = {} font_md5 = {} +font_data_buffer = None def calculate_md5(filepath): """计算文件的MD5哈希值""" @@ -44,7 +45,7 @@ def calculate_md5(filepath): def init_font_cache(): """初始化字体缓存和MD5""" - global font_cache, font_md5 + global font_cache, font_md5, font_data_buffer script_dir = os.path.dirname(os.path.abspath(__file__)) font_path = os.path.join(script_dir, FONT_FILE) @@ -55,24 +56,18 @@ def init_font_cache(): font_md5 = calculate_md5(font_path) print(f"Font MD5: {font_md5}") - # 预加载高频字到缓存 + # 加载整个字体文件到内存 + try: + with open(font_path, "rb") as f: + font_data_buffer = f.read() + print(f"Loaded font file into memory: {len(font_data_buffer)} bytes") + except Exception as e: + print(f"Error loading font file: {e}") + font_data_buffer = None + + # 预加载高频字到缓存 (仍然保留以便快速访问) for unicode_val in HIGH_FREQ_UNICODE: - try: - char = chr(unicode_val) - gb_bytes = char.encode('gb2312') - if len(gb_bytes) == 2: - code = struct.unpack('>H', gb_bytes)[0] - area = (code >> 8) - 0xA0 - index = (code & 0xFF) - 0xA0 - if area >= 1 and index >= 1: - offset = ((area - 1) * 94 + (index - 1)) * 32 - with open(font_path, "rb") as f: - f.seek(offset) - font_data = f.read(32) - if len(font_data) == 32: - font_cache[unicode_val] = font_data - except: - pass + get_font_data(unicode_val) print(f"Preloaded {len(font_cache)} high-frequency characters") # 启动时初始化字体缓存 @@ -104,6 +99,114 @@ THUMB_SIZE = 245 font_request_queue = {} FONT_RETRY_MAX = 3 +# 图片生成任务管理 +class ImageGenerationTask: + """图片生成任务管理类""" + def __init__(self, task_id: str, asr_text: str, websocket: WebSocket): + self.task_id = task_id + self.asr_text = asr_text + self.websocket = websocket + self.status = "pending" # pending, optimizing, generating, completed, failed + self.progress = 0 + self.message = "" + self.result = None + self.error = None + +# 存储活跃的图片生成任务 +active_tasks = {} +task_counter = 0 + + +async def start_async_image_generation(websocket: WebSocket, asr_text: str): + """异步启动图片生成任务,不阻塞WebSocket连接""" + global task_counter, active_tasks + + task_id = f"task_{task_counter}_{int(time.time() * 1000)}" + task_counter += 1 + + task = ImageGenerationTask(task_id, asr_text, websocket) + active_tasks[task_id] = task + + print(f"Starting async image generation task: {task_id}") + + await websocket.send_text(f"TASK_ID:{task_id}") + + def progress_callback(progress: int, message: str): + """进度回调函数""" + task.progress = progress + task.message = message + try: + asyncio.run_coroutine_threadsafe( + websocket.send_text(f"TASK_PROGRESS:{task_id}:{progress}:{message}"), + asyncio.get_event_loop() + ) + except Exception as e: + print(f"Error sending progress: {e}") + + try: + task.status = "optimizing" + + await websocket.send_text("STATUS:OPTIMIZING:正在优化提示词...") + await asyncio.sleep(0.2) + + optimized_prompt = await asyncio.to_thread(optimize_prompt, asr_text, progress_callback) + + await websocket.send_text(f"PROMPT:{optimized_prompt}") + task.optimized_prompt = optimized_prompt + + task.status = "generating" + await websocket.send_text("STATUS:RENDERING:正在生成图片,请稍候...") + await asyncio.sleep(0.2) + + image_path = await asyncio.to_thread(generate_image, optimized_prompt, progress_callback) + + task.result = image_path + + if image_path and os.path.exists(image_path): + task.status = "completed" + await websocket.send_text("STATUS:COMPLETE:图片生成完成") + await asyncio.sleep(0.2) + + await send_image_to_client(websocket, image_path) + else: + task.status = "failed" + task.error = "图片生成失败" + await websocket.send_text("IMAGE_ERROR:图片生成失败") + await websocket.send_text("STATUS:ERROR:图片生成失败") + + except Exception as e: + task.status = "failed" + task.error = str(e) + print(f"Image generation task error: {e}") + await websocket.send_text(f"IMAGE_ERROR:图片生成出错: {str(e)}") + await websocket.send_text("STATUS:ERROR:图片生成出错") + finally: + if task_id in active_tasks: + del active_tasks[task_id] + + return task + + +async def send_image_to_client(websocket: WebSocket, image_path: str): + """发送图片数据到客户端""" + with open(image_path, 'rb') as f: + image_data = f.read() + + print(f"Sending image to ESP32, size: {len(image_data)} bytes") + + # Send start marker + await websocket.send_text(f"IMAGE_START:{len(image_data)}:{THUMB_SIZE}") + + # Send binary data directly + chunk_size = 4096 # Increased chunk size for binary + for i in range(0, len(image_data), chunk_size): + chunk = image_data[i:i+chunk_size] + await websocket.send_bytes(chunk) + + # Send end marker + await websocket.send_text("IMAGE_END") + print("Image sent to ESP32 (Binary)") + def get_font_data(unicode_val): """从字体文件获取单个字符数据(带缓存)""" @@ -121,20 +224,27 @@ def get_font_data(unicode_val): if area >= 1 and index >= 1: offset = ((area - 1) * 94 + (index - 1)) * 32 - script_dir = os.path.dirname(os.path.abspath(__file__)) - font_path = os.path.join(script_dir, FONT_FILE) - if not os.path.exists(font_path): - font_path = os.path.join(script_dir, "..", FONT_FILE) - if not os.path.exists(font_path): - font_path = FONT_FILE - - if os.path.exists(font_path): - with open(font_path, "rb") as f: - f.seek(offset) - font_data = f.read(32) - if len(font_data) == 32: - font_cache[unicode_val] = font_data - return font_data + if font_data_buffer: + if offset + 32 <= len(font_data_buffer): + font_data = font_data_buffer[offset:offset+32] + font_cache[unicode_val] = font_data + return font_data + else: + # Fallback to file reading if buffer failed + script_dir = os.path.dirname(os.path.abspath(__file__)) + font_path = os.path.join(script_dir, FONT_FILE) + if not os.path.exists(font_path): + font_path = os.path.join(script_dir, "..", FONT_FILE) + if not os.path.exists(font_path): + font_path = FONT_FILE + + if os.path.exists(font_path): + with open(font_path, "rb") as f: + f.seek(offset) + font_data = f.read(32) + if len(font_data) == 32: + font_cache[unicode_val] = font_data + return font_data except: pass return None @@ -333,10 +443,13 @@ def process_chunk_32_to_16(chunk_bytes, gain=1.0): return processed_chunk -def optimize_prompt(asr_text): +def optimize_prompt(asr_text, progress_callback=None): """使用大模型优化提示词""" print(f"Optimizing prompt for: {asr_text}") + if progress_callback: + progress_callback(0, "正在准备优化提示词...") + system_prompt = """你是一个AI图像提示词优化专家。将用户简短的语音识别结果转化为详细的、适合AI图像生成的英文提示词。 要求: 1. 保留核心内容和主要元素 @@ -346,6 +459,9 @@ def optimize_prompt(asr_text): 5. 不要添加多余解释,直接输出优化后的提示词""" try: + if progress_callback: + progress_callback(10, "正在调用AI优化提示词...") + response = Generation.call( model='qwen-turbo', prompt=f'{system_prompt}\n\n用户语音识别结果:{asr_text}\n\n优化后的提示词:', @@ -356,31 +472,76 @@ def optimize_prompt(asr_text): if response.status_code == 200: optimized = response.output.choices[0].message.content.strip() print(f"Optimized prompt: {optimized}") + + if progress_callback: + progress_callback(30, f"提示词优化完成: {optimized[:50]}...") + return optimized else: print(f"Prompt optimization failed: {response.code} - {response.message}") + if progress_callback: + progress_callback(0, f"提示词优化失败: {response.message}") return asr_text except Exception as e: print(f"Error optimizing prompt: {e}") + if progress_callback: + progress_callback(0, f"提示词优化出错: {str(e)}") return asr_text -def generate_image(prompt, websocket=None): - """调用万相文生图API生成图片""" +def generate_image(prompt, progress_callback=None, retry_count=0, max_retries=2): + """调用万相文生图API生成图片 + + Args: + prompt: 图像生成提示词 + progress_callback: 进度回调函数 (progress, message) + retry_count: 当前重试次数 + max_retries: 最大重试次数 + """ print(f"Generating image for prompt: {prompt}") + if progress_callback: + progress_callback(35, "正在请求AI生成图片...") + try: response = ImageSynthesis.call( - model='wan2.6-t2i', - prompt=prompt, - size='512x512', - n=1 + model='wanx2.0-t2i-turbo', + prompt=prompt ) if response.status_code == 200: - image_url = response.output['results'][0]['url'] - print(f"Image generated, downloading from: {image_url}") + task_status = response.output.get('task_status') + + if task_status == 'PENDING' or task_status == 'RUNNING': + print("Waiting for image generation to complete...") + if progress_callback: + progress_callback(45, "AI正在生成图片中...") + + import time + task_id = response.output.get('task_id') + max_wait = 120 + waited = 0 + while waited < max_wait: + time.sleep(2) + waited += 2 + task_result = ImageSynthesis.fetch(task_id) + if task_result.output.task_status == 'SUCCEEDED': + response.output = task_result.output + break + elif task_result.output.task_status == 'FAILED': + error_msg = task_result.output.message if hasattr(task_result.output, 'message') else 'Unknown error' + print(f"Image generation failed: {error_msg}") + if progress_callback: + progress_callback(35, f"图片生成失败: {error_msg}") + return None + + if response.output.get('task_status') == 'SUCCEEDED': + image_url = response.output['results'][0]['url'] + print(f"Image generated, downloading from: {image_url}") + + if progress_callback: + progress_callback(70, "正在下载生成的图片...") import urllib.request urllib.request.urlretrieve(image_url, GENERATED_IMAGE_FILE) @@ -392,6 +553,9 @@ def generate_image(prompt, websocket=None): shutil.copy(GENERATED_IMAGE_FILE, output_path) print(f"Image also saved to {output_path}") + if progress_callback: + progress_callback(80, "正在处理图片...") + # 缩放图片并转换为RGB565格式 try: from PIL import Image @@ -422,21 +586,50 @@ def generate_image(prompt, websocket=None): f.write(rgb565_data) print(f"Thumbnail saved to {GENERATED_THUMB_FILE}, size: {len(rgb565_data)} bytes") + + if progress_callback: + progress_callback(100, "图片生成完成!") + return GENERATED_THUMB_FILE except ImportError: print("PIL not available, sending original image") + if progress_callback: + progress_callback(100, "图片生成完成!(原始格式)") return GENERATED_IMAGE_FILE except Exception as e: print(f"Error processing image: {e}") + if progress_callback: + progress_callback(80, f"图片处理出错: {str(e)}") return GENERATED_IMAGE_FILE else: - print(f"Image generation failed: {response.code} - {response.message}") - return None + error_msg = f"{response.code} - {response.message}" + print(f"Image generation failed: {error_msg}") + + # 重试机制 + if retry_count < max_retries: + print(f"Retrying... ({retry_count + 1}/{max_retries})") + if progress_callback: + progress_callback(35, f"图片生成失败,正在重试 ({retry_count + 1}/{max_retries})...") + return generate_image(prompt, progress_callback, retry_count + 1, max_retries) + else: + if progress_callback: + progress_callback(35, f"图片生成失败: {error_msg}") + return None except Exception as e: print(f"Error generating image: {e}") - return None + + # 重试机制 + if retry_count < max_retries: + print(f"Retrying after error... ({retry_count + 1}/{max_retries})") + if progress_callback: + progress_callback(35, f"生成出错,正在重试 ({retry_count + 1}/{max_retries})...") + return generate_image(prompt, progress_callback, retry_count + 1, max_retries) + else: + if progress_callback: + progress_callback(35, f"图片生成出错: {str(e)}") + return None @app.websocket("/ws/audio") async def websocket_endpoint(websocket: WebSocket): @@ -554,132 +747,36 @@ async def websocket_endpoint(websocket: WebSocket): # 先发送 ASR 文字到 ESP32 显示 await websocket.send_text(f"ASR:{asr_text}") - await websocket.send_text("GENERATING_IMAGE:正在优化提示词...") - # 等待一会让 ESP32 显示文字 - await asyncio.sleep(0.5) - - # 优化提示词 - optimized_prompt = await asyncio.to_thread(optimize_prompt, asr_text) - - await websocket.send_text(f"PROMPT:{optimized_prompt}") - await websocket.send_text("GENERATING_IMAGE:正在生成图片,请稍候...") - - # 调用文生图API - image_path = await asyncio.to_thread(generate_image, optimized_prompt) - - if image_path and os.path.exists(image_path): - # 读取图片并发送回ESP32 - with open(image_path, 'rb') as f: - image_data = f.read() - - print(f"Sending image to ESP32, size: {len(image_data)} bytes") - - # 使用hex编码发送(每个字节2个字符) - image_hex = image_data.hex() - await websocket.send_text(f"IMAGE_START:{len(image_data)}:{THUMB_SIZE}") - - # 分片发送图片数据 - chunk_size = 1024 - for i in range(0, len(image_hex), chunk_size): - chunk = image_hex[i:i+chunk_size] - await websocket.send_text(f"IMAGE_DATA:{chunk}") - - await websocket.send_text("IMAGE_END") - print("Image sent to ESP32") - else: - await websocket.send_text("IMAGE_ERROR:图片生成失败") + await start_async_image_generation(websocket, asr_text) else: print("No ASR text, skipping image generation") print("Server processing finished.") - elif text.startswith("GET_FONTS_BATCH:"): - # Format: GET_FONTS_BATCH:code1,code2,code3 (decimal unicode) + elif text.startswith("GET_TASK_STATUS:"): + task_id = text.split(":", 1)[1].strip() + if task_id in active_tasks: + task = active_tasks[task_id] + await websocket.send_text(f"TASK_STATUS:{task_id}:{task.status}:{task.progress}:{task.message}") + else: + await websocket.send_text(f"TASK_STATUS:{task_id}:unknown:0:任务不存在或已完成") + + elif text.startswith("GET_FONTS_BATCH:") or text.startswith("GET_FONT") or text == "GET_FONT_MD5" or text == "GET_HIGH_FREQ": + # 使用新的统一字体处理函数 try: - codes_str = text.split(":")[1] - code_list = codes_str.split(",") - print(f"Batch Font Request for {len(code_list)} chars: {code_list}") - - for code_str in code_list: - if not code_str: continue - - try: - unicode_val = int(code_str) - char = chr(unicode_val) - - gb_bytes = char.encode('gb2312') - if len(gb_bytes) == 2: - code = struct.unpack('>H', gb_bytes)[0] - else: - print(f"Character {char} is not a valid 2-byte GB2312 char") - # Send empty/dummy? Or just skip. - # Better to send something so client doesn't wait forever if it counts responses. - # But client probably uses a set of missing chars. - continue - - # Calc offset - area = (code >> 8) - 0xA0 - index = (code & 0xFF) - 0xA0 - - if area >= 1 and index >= 1: - offset = ((area - 1) * 94 + (index - 1)) * 32 - - # Read font file - # Optimization: Open file once outside loop? - # For simplicity, keep it here, OS caching helps. - - script_dir = os.path.dirname(os.path.abspath(__file__)) - font_path = os.path.join(script_dir, FONT_FILE) - if not os.path.exists(font_path): - font_path = os.path.join(script_dir, "..", FONT_FILE) - if not os.path.exists(font_path): - font_path = FONT_FILE - - if os.path.exists(font_path): - with open(font_path, "rb") as f: - f.seek(offset) - font_data = f.read(32) - if len(font_data) == 32: - import binascii - hex_data = binascii.hexlify(font_data).decode('utf-8') - response = f"FONT_DATA:{code_str}:{hex_data}" - await websocket.send_text(response) - # Small yield to let network flush? - # await asyncio.sleep(0.001) - except Exception as e: - print(f"Error processing batch item {code_str}: {e}") - - # Send a completion marker - await websocket.send_text("FONT_BATCH_END") - - except Exception as e: - print(f"Error handling BATCH FONT request: {e}") - await websocket.send_text("FONT_BATCH_END") # Ensure we unblock client - - elif text.startswith("GET_FONT_UNICODE:") or text.startswith("GET_FONT:"): - # 格式: GET_FONT_UNICODE:12345 (decimal) or GET_FONT:0xA1A1 (hex) - try: - is_unicode = text.startswith("GET_FONT_UNICODE:") - code_str = text.split(":")[1] - - target_code_str = code_str # Used for response - - if is_unicode: - unicode_val = int(code_str) - char = chr(unicode_val) - try: - gb_bytes = char.encode('gb2312') - if len(gb_bytes) == 2: - code = struct.unpack('>H', gb_bytes)[0] - else: - print(f"Character {char} is not a valid 2-byte GB2312 char") - continue - except Exception as e: - print(f"Failed to encode {char} to gb2312: {e}") - continue + if text.startswith("GET_FONTS_BATCH:"): + await handle_font_request(websocket, text, text.split(":", 1)[1]) + elif text.startswith("GET_FONT_FRAGMENT:"): + await handle_font_request(websocket, text, text.split(":", 1)[1]) + elif text.startswith("GET_FONT_UNICODE:") or text.startswith("GET_FONT:"): + parts = text.split(":", 1) + await handle_font_request(websocket, parts[0], parts[1] if len(parts) > 1 else "") else: - code = int(code_str, 16) + await handle_font_request(websocket, text, "") + except Exception as e: + print(f"Font request error: {e}") + await websocket.send_text("FONT_BATCH_END:0:0") # 计算偏移量 # GB2312 编码范围:0xA1A1 - 0xFEFE diff --git a/websocket_server/test_generated_thumb.bin b/websocket_server/test_generated_thumb.bin new file mode 100644 index 0000000..8857cb5 --- /dev/null +++ b/websocket_server/test_generated_thumb.bin @@ -0,0 +1,154 @@ +bbbb(8BIa#z䚣rY"I"ICQcYaaiqqzz#DEgԈ )JJkkKKkkkjj:[|{9tLr7yxXW6qPP012222կnL h(Ǜfɋ +KLlllK +{sGskbbZXWqf%Dê#bq!aAayBcyqiiabYBY"QIIH@@@HH@@@H!YaaiabYBQ"IIA8( a a ((0880(( abb (9bQiCz䚃qbY"IBIcQYaiiqqzz#de&gԨ *JJkkkKKkkjjQ;|[SLq7{yxX76qP002STTSS2կm,뼩HƓff*KllL+郈{GskbbZW7qf%Dê#aq!aAayBCyqiiabaBY"Q!IIHH@HHH@@@H!YaaiabYBQ"I"IA8( a a ((08880( abb 0AYrcÒĒCibQ"IBIcQYaiiqqz#DĢ%GԇԨ )*JkkkkJJJjjjj0:[|[TKQ7{yxxW76qqP0001STuuuuTT3կm,뼩H盦ffȋ+lK +ȃg{&skjZW7qf%Dê#bq!aAiBCyqqiabaBY"Q!QIHHHHHH@@@H!YbaiabYBQ"I"IA8( a ((08880( a (8BIa#z䚤#zaBQ"IBIcQYaiiqqz#De&gԧ )*JJkkkkJJJJjjjr;||[SKQ7{yxXW76qqPP00RtuuvuT3կՎM,꼩H盦ffȋ Klͤ*苧F{&skbW7qf%Dê#bq!aAiBCyqqiabaBYBQ"QIIHHHHHH@@Q!YbiiabYBQ"I"IA8( ((08880( a (AbQiC䚄rYBI"IBQcYYaiiqqz$dĢ$Gԇ )JJkkkkJJJJJJJjQ:[||[S+Q{yyxxXW6qqQP01Suvu4կmL 꼉H盦 K̤ͤͤͤiNjF{%sbƋW7qf%Dê#bq!aAiBcyqqiibabYBQ"QIIHHHHHH@HQ!YbiiibYBQ"I"IA80 ((08880( a 0"AYrcĒ䚃qbYBIBIcQYaaiiqyCe&gԨ )*JjkkkjJJJJIIIs[||[3+lP{yyxxXW76qqQQQRtvUՎm, ʼ(ƛNj)kͤˬi(ƋE{jƓWWqf%Dê#bq!aAiBc#yyqiibabYBY"QIIHHHHHH@HQ!YbiiibaBQBQ"IA80( ((08880( a (8BIa#zĚCiBQ"IBIcQYaaiqqz#cĚ$gԧ )*JjkkkJJ*JII)IR;||[3n l0rzyyxxXWW76qqqQQruՎM, ʼhǛǓǓJ̤ˬhG擥$s哰WWqf%Dê#bq!aAibc#yqiiabYBY"QIIHHHHHH@HQAYbiiibaBQBQ"IA80( ((08880( a  (AbQiCÒĒ#zaBQBIBQcYYaiiqqzCE&̇Ԩ )*JjkkJJ*)II)(:\}|[3n +k0rzyxxxXWW76rrrsv3ՎL 꼉HǛǛ)jʬG%WWf%Dê#aq!iaibc#yqqiabaBY"Q!QIIHHHHHHHQAYbiiibaBYBQ"II80( ((08880( a  (0"IYr䚣zYBIBIcQYaaiiqy#c$Ģ )JjkjJ*))))(IR;||;3N +K0qzxxxxxXXW774n,ʴH()j̤ʬFƬHͩhhWX f%Dê#aq!iaqbòc#yqqiabaBY"Q"QIIHHHHHHHQAYbiiibaBYBQ"I"I@0( ((088800(a  (8BQa#zcqbYBIBIcQYaaiiqz#D&gԨ )JJkkJJ))))((0:\}|;MKQrzxxxWWWW76Un,ʴi((Ij̬ʬɬKlK+ +wX f%Dò#‰aqAiaq≂òc#yyqiabaBYBQ"QQIHHHHHHHQAYbiiibaBYBQ"I"I@0( ((088880(a 0AbYicÒĚCiBQBIBQcQYaaiiqCĢ )JjkJ))))(IS;|}[;MKQrZxxXWWW766:;V֏M 봪H((IIˤˬʬɬ͍lKKK ++wX)f%Dò#‰bqAiaqòc#yyqiibabYBY"QQIIHHHHHHQAYbiqibabYBQ"I"IA0( ((088880( (8"Iar$#zaBQBIBQcYYaaiqy#cĢDg )JjkjJ)))((\|}|[:-+QrrZxWWW7666;;;vְN- ˴I((HiˤˤˬʬhkJKwX)%Dò#‰bqAiqúcCyqiiabYBY"Q!QIIHHHHHHQAabiqibabYBQ"Q"IA0( ((08@@80( (8BQiCz$rYBIBIbQYYaaiqzCeḞ)JjkJ)))(IS;\|}|[:u +0QrpZ|yxWW766:;;vְn- 봪i(((Hiˤ̤ˤʤǬ+ޭ2wX)%DòC‰bqAiqúòCyyqqiabYBY"Q!QQIIQHHHHQAaiqiabYBQBQ"IA8(( ((08@@80( (0AbYqc$$cibQBIBQbQYYaiiqcĚ$gԧ )JkjJ))(t;\\|}||||||||[u ݩ +0QqrqppZ|xxWW666:2nM-ˬiH(Hˬ --- ̤ʤl2wX)%DòC‰bqAiyòC#yqqiabaBYBQ!QQIIQHHHHQAaiqiabYBQBQ"IA8(( ((08@@80( (8"Ia#z$$ĚCaBQBIBQcYYaaiqy#eḞ)JjjI))I2v;\\\|||||||||;uܩ +l0QQqqpp9{xxXW6q0ޯM- ˴iHiˬ ˤI3xX)%DòC‰bqAiy"òC#yqqiabaBYBQ"QQIIQQHHHQAaiqqabYBQBQ"IA8(( ((08@@80( (@BQiCÒ$D$zaBIBIbQcYYaaiqC$gԧ)jjI)(kS;[\\[[[\\||[;U܉ +l0QqqPPq9{yxxWW6oލ- 봪iˬˬ̬ ˤjŭ3wX)%DòC‰bqAiy"òC#yyqiibabYBY"Q!QQQQQHHHQAaiqqabYBYBQ"IA8(( ((08@@80(( (0AbYqc$DqbYBIBIbQcYYaaiy#cd&̇)JjjI(IT;;;;;;[[[\\[:Uԉl0PQPPOp[|xxxXWW66ooNL 봪ˬˤˬ 줩jŮwX)%DòC‰bqAiy"òC#yyqiibabYBY"Q!QQQQQHHHQAaiqqibYBYBQ"IA80( ((08@A@80( (8"Ia"zDDCibQBIBQbQcYYaiqzC$F̧IjkJ)(kU:;[[\[UiK0PPOOPZ{xxxXWW766rrronnnNNl 봪ˬˬˬ -- jwX)%DòC‰bqAiBòúC#yyqiibabYBYBQ!QQQQQHHH!YAaiqqibabYBQ"QI80((((08@A@80( (@BQiCÒ$D$Ē#zaBQBIBQbYcYaaiy#cDŻ&̇)J  jI)J3u;;[[;4hK000//P:[xxxWWW766rrqqqrrrrssssrRQponnnmMMM-l ʴˬ -.NN. ˜KwX)%DòC‰byAqBòúC#yqiiabYBYBQ!QQQQQPHP!YAaiqqibabYBQBQI80((((088A@80((0AYqDD$rYBQBIbQbYcYaiqzCf̧Ij  jI( k3u;;[;4hK0//.P9Z{yxxWWWW666rrqqqQQQQQQQQ10/ONNMMMM---K ʴˬ .NoooO ,nkkWX)%dòC‰byaqBúúC#yqqiabYBYBY!QQQQQPHP!YbaiqqibabYBQBQ"I80((((088A@80((8"Ia"z$dDcqbYBIBQbQbYaaiq#cDŻ&̇)j jIJ4uvvvޕޕޕ:;;;4H+/Z{{zyyxWWW6666rqqqQQQQQ110.-----, + +ʴʴ -.NOopO/ ֯lJ*VX)%DòC‰ayaqBúúC#yqqiabaBYBY"Q!QQQQPHP!YaaiqqibabYBQBQ"I80((((088A@80((8BQiCDdDCiBQBIBQbYbYaiqzC$F̧(Ij ..J( K4UuUTTttt:;p݊(̩+/s:ZzzzyxWW766rqqQQQQQQ11 K* +ʴˬ ./PpPP/mNlKVX*%DòC‰ayaqb㺃C#yqqiabaBYBY!Y!QQQQPHP!YaaiqqiabYBQBQ"I80((((088A@80(0AYrddDĒ#zaBQBQBQbYYaiq#DŻ&̇)j...jI(*lݮ34T332SSst:woj(̩+s9ZZzyXXW66rqQQQQQQ11խխ݋K+ +ʴˬ/PPqP0 -, gGհVX*f%DòC‰ayaqb㺃C#yyqiababYBY"Y!QQQQPHQ!YbaqqqiabYbQBQ"I@0((((088@A@808"Ia"zÒ$d$qbYBQBQbQbYaiqzC$F̧Ij ....j()Kݍݯ33312RsޕvOj(ĉ R:ZYYXW66rRQQQRR11ծՍՌl͋ͬիիՋK* + +ɴˬ̬̬̤/PQQ0-͋j)͇ǼVXJf%DòC‰ayaqb㺃C#yyqiababYBYBY!QQQQQPQ!YbaqqqiabYbYBQ"I@0((((088@A@80AbQicddcibQBIBQbYYaiy#dŻ&̇)j... I*lݎ2StޕVOIĉ +lR9YYX776rrRRRRR1խՍlK+ + + + + +ɴɴɴˬ̤̤̤PPP0Μj){FVXJf%DòC‰ayaq≂㺃c##yyqiibabYBYBY!QQQQQPQ!YbaqqqiabYbYBQ"IA0((((008@A@80"IYr$d#aBQBQBQbYaiqzcĚ$F̧I ....j()Kmկ2StV.)ĉ +lR9987srrRRR22ݮՍlL+ + +ɴhhhiͤ/000ϜL |s&cGVXJf%DòC‰ayay≂㺃c##yyqiibabYBYBY!Q!QQQQPQ!YbiqyqiabYbYBQ"IA0((((008@A@80BQiCÒDDzYBQBQbQbYaiy#dŻ&̆)j.... I +LՎհ2TuwU.)il2987ssrRRR22ծՍl+ +ɼɼɴhhhHHHHIjjjj00ϔM sGcgVXJf%DòC‰ayay⑂㺃cC#yyqiibabaBYBY!Q!QQQQPQ!YbiqyqiabYbYBQ"IA8(((((08@A@80bYqcd$qbYBQBQbYaiqcĚ$F̧I ....j(*Ln͏Ͱ2Stw5)hLm1usssRRR2կՎmK + +ɼɼhH'''''(IIJJkͤϜM |sGcgVyJf%DòC‰ayy⑂㺃cC#yqiiabaBYBY!Q!Q!QQQPQ!YbiqyqiabYbYbQBQA8(((((08@A@88a#z$dCiBQBQBQbYaiy#dŻ&f)j ../.. I +M͏Ͱ3TuV5 ǻhLluqqqrsssssRR21ՎmL+ +ɼɼhG'ǓƓȋ)Jk͜ΜΜϜϜϜM {gkVyJ%DòC‰ayy⑂#㺣cC#yqiiabaBYBY!Q!Q!QQQPQ!YbiqyyqababYbQBQA8(((((08@A@88icdDĚ#zaBQBQbQYiqcÚ$ḞI .N..I( *MnͰű2SuVǻH,lllUqqqqQQQqqrrsssssssssRR21ծՎm+ +ɼɼh'ƛƓfF{F{F{{ JKlϔϔm,{hkWyj%DòC‰yy#£cC#yqiiababYBY!Y!Q!YYQPQ!YbiqyyqababYbYBQA8(((((08@AA@8r$$qbYBQBQbYaiyCdŻ%fԧ(i .... j) +,n͐ű2SuwVǻH+LllllUqqqqQPPPPPPQQRrrrrsssRR21կՎmL+ɼhG'ƛffE%{%{sss&sFsgs{+mn, |kWyjf%DòC‰yy##ãcC#yqqiababYBY!Y!Q!YYQPQAYbiqyyqibabYbYBQI80((((08@AA@8CÒDŻcibYBQBQbYaqcÚ$F̆I.... I ++Moű2TuwVdzH+LLlllklTvqqqqPPP00000001QQQRRRSRR21ծՎl+ +鼩hG'ƓffE%{%{srrkksGshs{ LM |sɬWyj%dòC‰yy#$ãcC#yqqiababYBY!Y!Q!YYQPQAYbiqyyqiabYbYBQI80((((08@AA@8Ļd#aBQBQbYaiy#dŻ%f̧(i ..I) + -oőű2Tuv5pȳdz(Ī+LLlllKKTvqqpPPPP00000001QQQRRR21ՎmL+ɼhH'ƛfFE%{ssjjjjk&sgs{ +Lmm,|sWyj%dC‰y#$ãcC#yqqiababaBY!Y!Y!YYQPQAaiqyyqiabYbYBQI80((((08@AA@8Ò$ĻDzaBQBQbYaqzcÚ$F̆I .. i) + ,NŐűŲ3Tuv5pլȫ(ĩ ,LLllKK4vqPPPP000000011112211ծՍL+ ɴhH'ƛƓffF%{%{srjjjjk&kGss |LϔnMs +wyj%dC‰y"ò#DãcC#yyqiibabaBY!Y!Y!YYQPQAaiqyyqiabYbYBQ"I80((((08@AAA8dĻcqbYBQbYaiy#Df̧(i . jI + .pŲ3TuޖV5pլȫ(ĩ ++LLK+Km4vrPPP0000000011221ծծmL+ +ɴhH'ƓffFF{%{srjjjjk&kGks |MϔДДДM{ wyk%dC‰y"ú#DãcC#yyqiiabaBY!Y!Y!YYQQYAaiqyyqiabYbYBQ"I80((((08@AAA@$ĻĻdCibYBQbYaqzC$&̆I  I) -OpŲ3TuޖV5Pլȫ(ĉ ++KLK++m4vrP000000000001111ծmL+ +ɴhH'ǓƓfffF{&{ssrjjjk'khk{,nДn +|+wyk%dC‰y"úCDãC##yyqiiabaBY"Y!Y!YYQQYAaiqyyqiabYbYBQ"I@0((((08@AIA@dĻDzaBQBQYiq#DF̧(i  i) + .pqűű4uޖV5PՌĉ ++KK*+lVrP000000000011001111ծՍl+ +ɴhH(ǓfffF{&{&sssjjkk'khs{MД+|+wyk%dc⑂y"úCDãcC##yqiiabaBY"Y!Y!Y!YQQYAaiyyyqiabYbYBQ"I@0((((08@AIA@ĻĻ$qYBQbYaizCdŻ&̆I jJJ+,-.OppűűŰŰŰŰŰͰͰ3UޖV5P͌ +++ ++LVwsQ000000011QQQQ11101111ծmL,+ +괩h(瓧fffF{&{&sssjjk'kGks |nϔ2222𔯌KLxyk%dc⑂BCDãC##yqiiabaBYBY!Y!Y!YQQ!YAaiyyyqiabYbYBQ"I@0((((08@AIA@ĻĻdCibYbYbYiq#cÚDF̦(i jklmnOPPPppoooŏŏŏŏŏŏůͯͯ3UޖV5P͋ +++ + LVwP///0001QRRRRRRR1111111ծ͎mL+ ʴhHǓffffF{&{&ssskkk'kHks,2SSSS2όLlxyk%dc⑂BDDòCC#yqiiabaBaBY!Y!Y!YQQ!YAaqyyyqiabYbYBY"QA0((((08@AIA@ĻDÚ#zabYbYaizCdŻ%fIݰ͑pPPPpppooNNMMMMmmmnŎů3UvޗV5P͋i + + +L6w0011RRsssssSRR100111ծ͍mL+ 괩HǓffff{F{&{&ssskkkGkhssmϔRsssR1lmxykf%dc⑂BDDòcC#yqiiabaBaBY!Y!Y!YQQ!YAaqyyyqiababYbY"QA0((((08@AIA@ĻĻ$qabYbYaq#cÚ$Ĕ(iͲpPPPpoONM--,,,,,LMnŎů3UޗW5P͋i + + + +,5w601RRstttttsSR1100011ծͮmL, ʴh(NjfffF{F{&ssskkk'kGks{n2ss2Дmxykf%dc⑂BCDIJcC#yqqiabaBaBY!Y!Y!YQQ!YAaqyyqiababYbY"QA8((((088AIA@ĻdCibYbYaiyCdŻfHi4UU5ŲqPOOOOON--- ,,LmŎů4Uޗw6Pkfi +,ݏ5Vy0RSstttttsSR1000͍ͮlL+ ʴiHNjfff{f{F{&{&sskkkk'kHks |Rs1Rmxykf%dc③BCDIJcC#yqqiibaBaBY!Y!Y!YQQ!YbaqyyqiababYbY"QA8((((088AIA@ĻDzabYbYaqzcÚ$F̆(ih2UvwVӵqPOOOOONN-- ,,MŎůTvޗw6Pkfi ++ݎ5V8wp0SsttssR1000͍ͮmL+ ʬH瓧ff{F{F{F{&sskkjjk'khss |RsƑkxykfdc③‰BDDIJcC#yqqiibabaBY!Y!Y!YQQ!YbaqyyqiababYbY"QA8((((008AAA@cqabYaiqCDfHiHh2u޹x6POOOOOON- ,LmŏUvޘw6Pkfi + + nVwx5.sttssRQ00ծ͍͍lL+ ꬩi(Njff{F{F{&sssjjjjk'khss,|RԵյsxykfdc③‰BDDIJcC#yqqiibabaBY!Y!Y!YQQ!YbaqyyqiababYbYBQA80(((008@IIADÚ#ibYbYaizcÚĔ(iiHHuޘWPOOPPpON. LmŎŰ3U֗޸w6Pkfi + nVwy{rssRQ00͍ͭlL+ 괪HNjgf{F{&s&ssjjjjjk'khss+|1qtѽ͔xykfܤdc③‘bcdIJcC#yqqiibabaBY!Y!Y!YYQ!YbiqyyqiababYbYBQA80(((008@AIAdzabYaiq#DfHiiiH'iT޹wP/PPppOO.- ,m4V֗޸w6PkfHĪ + nVwźmprtsrQ10͍ͭlL+ ʬI(狧gF{&s&ssjjjjjjk'kHss |mϔQ0άoŴr͌mxyjFܤDc③bddIJcC#yyqiibabaBY!Y!Y!YYQ!YbiqyyqiaabYbYBQI80(((008@AIADCqabYaizCdĔ(iiI('2<<wP/PpppoO.- ,M4v֗޸w6PkgfdzHĪ nVw9@BARssRQ00͍ͭͭŌL+ ʬi(苧g{F{&sskjjjbjjk'sGss{LmN10ҽ͌j|mxyjFܤDc③bdd$ĺcC##yyqiibaBaBY!Y!Y!YYY!YbiqyyqiababYbYBQI80(((008@AIA$zibYYiq#DfHijjiI(iu<\<<wPPPqqppOO.- ˴˴,M4v֘޹w6PkgfǫH nVw$@ZQrrQQ0͍ͮͭōlL+ ʬi(苧{Fs&skjjbbbjjk'sGss{H{ +[/klxykFܤDc③ᑂ#dd$ĺcC##yyqiibabaBY!Y!Y!YYY!YbiqyqiaabYbYBQI80(((008@AIAcqabYaizCdŻ%̆(IiiiIH'R<\\\<wPPPqqqppO.- ˬ,M4v֘޹w6PkgFǫHN6wO%C`҅0rQQ00͍ͮͭōllK+ +ꬪi(ǃ{Fs&skjjbbbjjk&sGsk&{q8pĉ[/k|LxykEܤDc③⑂#dd$ĺcCC#yyqiibabaBY!Y!Y!YYY!YbiqqiibabYbYBQI80(((008@AIA#ziaYaq"cÚ$e̦(IiiH(j;\\\\