diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml new file mode 100644 index 0000000..0ec38d6 --- /dev/null +++ b/.gitea/workflows/deploy.yaml @@ -0,0 +1,37 @@ +name: Deploy WebSocket Server + +on: + push: + branches: + - main # 或者是 master,请根据您的分支名称修改 + +jobs: + deploy: + runs-on: ubuntu # 或者您的 Gitea runner 标签 + steps: + - name: SSH Remote Commands + uses: appleboy/ssh-action@v1.0.0 + with: + host: ${{ secrets.SERVER_HOST }} + username: ${{ secrets.SERVER_USERNAME }} + password: ${{ secrets.SERVER_PASSWORD }} + port: 22 + script: | + # 进入项目目录 + cd /root/V2_micropython/ + + # 拉取最新代码 + echo "Pulling latest code..." + git pull + + # 进入 websocket_server 目录 + cd websocket_server + + # 重启服务 + echo "Restarting Docker Compose services..." + docker compose down + docker compose up -d + + # 检查部署状态 + echo "Checking deployment status..." + docker compose ps diff --git a/display.py b/display.py index e173884..94e4404 100644 --- a/display.py +++ b/display.py @@ -211,7 +211,37 @@ class Display: self.tft.line(x, y + 5, x + 3, y + 8, st7789.GREEN) self.tft.line(x + 3, y + 8, x + 10, y, st7789.GREEN) - def render_confirm_screen(self, asr_text=""): + def measure_text(self, text): + """计算文本宽度""" + width = 0 + for char in text: + if ord(char) > 127: + width += 16 + else: + width += 8 + return width + + def draw_centered_text(self, text, x, y, w, h, color, bg=None): + """在指定区域居中显示文本""" + if not self.tft: return + text_width = self.measure_text(text) + start_x = x + (w - text_width) // 2 + start_y = y + (h - 16) // 2 + + # 确保不超出边界 + start_x = max(x, start_x) + + if bg is not None: + self.tft.fill_rect(x, y, w, h, bg) + self.text(text, start_x, start_y, color) + + def draw_button(self, text, x, y, w, h, bg_color, text_color=st7789.WHITE): + """绘制带居中文字的按钮""" + if not self.tft: return + self.tft.fill_rect(x, y, w, h, bg_color) + self.draw_centered_text(text, x, y, w, h, text_color) + + def render_confirm_screen(self, asr_text="", waiting=False): """渲染确认界面""" if not self.tft: return @@ -220,12 +250,14 @@ class Display: # Header self.tft.fill_rect(0, 0, 240, 30, st7789.CYAN) - self.text("说完了吗?", 75, 8, st7789.BLACK) + self.draw_centered_text("说完了吗?", 0, 0, 240, 30, st7789.BLACK) # Content box self.tft.fill_rect(10, 50, 220, 90, 0x4208) # DARKGREY - if asr_text: + if waiting: + self.draw_centered_text("正在识别...", 10, 50, 220, 90, st7789.YELLOW) + elif asr_text: # 自动换行逻辑 max_width = 200 lines = [] @@ -254,18 +286,119 @@ class Display: for i, line in enumerate(lines): # 计算水平居中 - line_width = 0 - for c in line: - line_width += 16 if ord(c) > 127 else 8 - + line_width = self.measure_text(line) center_x = 20 + (200 - line_width) // 2 self.text(line, center_x, start_y + i * 20, st7789.WHITE, wait=False) else: - self.text("未识别到文字", 70, 85, st7789.WHITE) + self.draw_centered_text("未识别到文字", 10, 50, 220, 90, st7789.WHITE) # Buttons - self.tft.fill_rect(20, 160, 90, 30, st7789.GREEN) - self.text("短按确认", 30, 168, st7789.BLACK) + self.draw_button("短按确认", 20, 160, 90, 30, st7789.GREEN, st7789.BLACK) + self.draw_button("长按重录", 130, 160, 90, 30, st7789.RED, st7789.WHITE) + + def render_recording_screen(self, asr_text="", audio_level=0, is_recording=False): + """渲染录音界面""" + if not self.tft: + return - self.tft.fill_rect(130, 160, 90, 30, st7789.RED) - self.text("长按重录", 140, 168, st7789.WHITE) + self.tft.fill(st7789.BLACK) + + self.tft.fill_rect(0, 0, 240, 30, st7789.WHITE) + self.draw_centered_text("语音识别", 0, 0, 240, 30, st7789.BLACK) + + self.draw_mic_icon(105, 50) + + if audio_level > 0: + bar_width = min(int(audio_level * 2), 200) + self.tft.fill_rect(20, 100, bar_width, 10, st7789.GREEN) + + if asr_text: + self.text(asr_text[:20], 20, 130, st7789.WHITE, wait=False) + + if is_recording: + self.draw_button("松开停止", 60, 200, 120, 25, st7789.RED, st7789.WHITE) + else: + self.draw_button("长按录音", 60, 200, 120, 25, st7789.BLUE, st7789.WHITE) + + def render_result_screen(self, status="", prompt="", image_received=False): + """渲染结果界面""" + if not self.tft: + return + + if status == "OPTIMIZING": + self.tft.fill(st7789.BLACK) + self.tft.fill_rect(0, 0, 240, 30, st7789.WHITE) + self.draw_centered_text("AI 生成中", 0, 0, 240, 30, st7789.BLACK) + + self.draw_centered_text("正在思考...", 0, 60, 240, 20, st7789.CYAN) + self.draw_centered_text("优化提示词中", 0, 80, 240, 20, st7789.CYAN) + self.draw_progress_bar(40, 110, 160, 6, 0.3, st7789.CYAN) + + elif status == "RENDERING": + self.tft.fill(st7789.BLACK) + self.tft.fill_rect(0, 0, 240, 30, st7789.WHITE) + self.draw_centered_text("AI 生成中", 0, 0, 240, 30, st7789.BLACK) + + self.draw_centered_text("正在绘画...", 0, 60, 240, 20, st7789.YELLOW) + self.draw_centered_text("AI作画中", 0, 80, 240, 20, st7789.YELLOW) + self.draw_progress_bar(40, 110, 160, 6, 0.7, st7789.YELLOW) + + elif status == "COMPLETE" or image_received: + # Don't clear screen, image is already there + self.tft.fill_rect(230, 230, 10, 10, st7789.GREEN) + + elif status == "ERROR": + self.tft.fill(st7789.BLACK) + self.tft.fill_rect(0, 0, 240, 30, st7789.WHITE) + self.draw_centered_text("AI 生成中", 0, 0, 240, 30, st7789.BLACK) + self.draw_centered_text("生成失败", 0, 50, 240, 20, st7789.RED) + + if prompt and not image_received: + self.tft.fill_rect(10, 140, 220, 50, 0x2124) # Dark Grey + self.text("提示词:", 15, 145, st7789.CYAN) + self.text(prompt[:25] + "..." if len(prompt) > 25 else prompt, 15, 165, st7789.WHITE) + + if not image_received: + self.draw_button("长按返回", 60, 210, 120, 25, st7789.BLUE, st7789.WHITE) + + def draw_loading_spinner(self, x, y, angle, color=st7789.WHITE): + """绘制旋转加载图标""" + if not self.tft: + return + + import math + rad = math.radians(angle) + + 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)) + + self.tft.pixel(px, py, color) + + def draw_progress_bar(self, x, y, width, height, progress, color=st7789.CYAN): + """绘制进度条""" + if not self.tft: + return + + self.tft.fill_rect(x, y, width, height, 0x4208) # DARKGREY + if progress > 0: + bar_width = int(width * min(progress, 1.0)) + self.tft.fill_rect(x, y, bar_width, height, color) + + def draw_mic_icon(self, x, y, active=True): + """绘制麦克风图标""" + if not self.tft: + return + + color = st7789.GREEN if active else 0x4208 # DARKGREY + + self.tft.fill_rect(x + 5, y, 10, 5, color) + self.tft.fill_rect(x + 3, y + 5, 14, 10, color) + self.tft.fill_rect(x + 8, y + 15, 4, 8, color) + self.tft.fill_rect(x + 6, y + 23, 8, 2, color) + self.tft.fill_rect(x + 8, y + 25, 4, 3, color) diff --git a/main.py b/main.py index 000402f..65a6e7d 100644 --- a/main.py +++ b/main.py @@ -79,7 +79,7 @@ def connect_wifi(display=None, max_retries=5): # 简单的加载动画 if display and display.tft: if time.ticks_ms() % 200 < 50: # 节流刷新 - draw_loading_spinner(display, 120, 150, spinner_angle, st7789.CYAN) + display.draw_loading_spinner(120, 150, spinner_angle, st7789.CYAN) spinner_angle = (spinner_angle + 45) % 360 if wlan.isconnected(): @@ -108,164 +108,6 @@ def connect_wifi(display=None, max_retries=5): return False -def draw_mic_icon(display, x, y, active=True): - """绘制麦克风图标""" - if not display or not display.tft: - return - - color = st7789.GREEN if active else 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, 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, is_recording=False): - """渲染录音界面""" - 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, wait=False) - - display.tft.fill_rect(60, 200, 120, 25, st7789.RED) - if is_recording: - display.text("松开停止", 85, 205, st7789.WHITE) - else: - 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, 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 - - if status == "OPTIMIZING": - display.tft.fill(st7789.BLACK) - display.tft.fill_rect(0, 0, 240, 30, st7789.WHITE) - display.text("AI 生成中", 80, 8, st7789.BLACK) - - 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.tft.fill(st7789.BLACK) - display.tft.fill_rect(0, 0, 240, 30, st7789.WHITE) - display.text("AI 生成中", 80, 8, st7789.BLACK) - - 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: - # Don't clear screen, image is already there - - # Draw a small indicator to show it's done, but don't cover the image - # Maybe a small green dot in the corner? - display.tft.fill_rect(230, 230, 10, 10, st7789.GREEN) - - elif status == "ERROR": - display.tft.fill(st7789.BLACK) - display.tft.fill_rect(0, 0, 240, 30, st7789.WHITE) - display.text("AI 生成中", 80, 8, st7789.BLACK) - display.text("生成失败", 80, 50, st7789.RED) - - if prompt and not image_received: - 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) - - # Only show back button if not showing full image, or maybe show it transparently? - # For now, let's not cover the image with the button hint - if not image_received: - display.tft.fill_rect(60, 210, 120, 25, st7789.BLUE) - display.text("长按返回", 90, 215, st7789.WHITE) - - - - - @@ -368,9 +210,6 @@ def process_message(msg, display, image_state, image_data_list): 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, wait=False) def get_boot_button_action(boot_btn): @@ -459,6 +298,7 @@ def main(): current_status = "" image_generation_done = False confirm_waiting = False + recording_stop_time = 0 def connect_ws(force=False): nonlocal ws @@ -502,13 +342,13 @@ def main(): # WiFi 和 WS 都连接成功后,进入录音界面 ui_screen = UI_SCREEN_RECORDING if display.tft: - render_recording_screen(display, "", 0, False) + display.render_recording_screen("", 0, False) else: print("Running in offline mode") # 即使离线也进入录音界面(虽然不能用) ui_screen = UI_SCREEN_RECORDING if display.tft: - render_recording_screen(display, "离线模式", 0, False) + display.render_recording_screen("离线模式", 0, False) read_buf = bytearray(4096) last_audio_level = 0 @@ -533,18 +373,25 @@ def main(): if time.ticks_diff(now, last_spinner_time) > 100: if display.tft: # Clear previous spinner (draw in BLACK) - draw_loading_spinner(display, 110, 80, spinner_angle, st7789.BLACK) + display.draw_loading_spinner(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) + display.draw_loading_spinner(110, 80, spinner_angle, color) last_spinner_time = now btn_action = get_boot_button_action(boot_btn) + # ASR timeout check + if ui_screen == UI_SCREEN_CONFIRM and confirm_waiting: + if time.ticks_diff(time.ticks_ms(), recording_stop_time) > 2000: + confirm_waiting = False + if display.tft: + display.render_confirm_screen("", waiting=False) + # Hold to Record Logic (Press to Start, Release to Stop) if ui_screen == UI_SCREEN_RECORDING: if boot_btn.value() == 0 and not is_recording: @@ -556,7 +403,7 @@ def main(): current_status = "" image_generation_done = False if display.tft: - render_recording_screen(display, "", 0, True) + display.render_recording_screen("", 0, True) if ws is None or not ws.is_connected(): connect_ws() if ws and ws.is_connected(): @@ -574,8 +421,13 @@ def main(): is_recording = False ui_screen = UI_SCREEN_CONFIRM image_generation_done = False + + # 启动等待计时 + confirm_waiting = True + recording_stop_time = time.ticks_ms() + if display.tft: - render_confirm_screen(display, current_asr_text) + display.render_confirm_screen(current_asr_text, waiting=True) # Consume action to prevent triggering other events btn_action = 0 @@ -591,7 +443,7 @@ def main(): ui_screen = UI_SCREEN_RESULT image_generation_done = False if display.tft: - render_result_screen(display, "OPTIMIZING", current_asr_text, False) + display.render_result_screen("OPTIMIZING", current_asr_text, False) time.sleep(0.5) elif btn_action == 2: @@ -603,7 +455,7 @@ def main(): is_recording = False image_generation_done = False if display.tft: - render_recording_screen(display, "", 0, False) + display.render_recording_screen("", 0, False) time.sleep(0.5) elif btn_action == 3: @@ -637,12 +489,13 @@ def main(): if event_data[0] == "asr": current_asr_text = event_data[1] print(f"Received ASR: {current_asr_text}") + confirm_waiting = False # 收到 ASR 结果,跳转到 CONFIRM 界面 if ui_screen == UI_SCREEN_RECORDING or ui_screen == UI_SCREEN_CONFIRM: ui_screen = UI_SCREEN_CONFIRM if display.tft: - render_confirm_screen(display, current_asr_text) + display.render_confirm_screen(current_asr_text, waiting=False) elif event_data[0] == "font_update": # 如果还在录音界面等待,刷新一下(虽然可能已经跳到 CONFIRM 了) @@ -652,21 +505,21 @@ def main(): current_status = event_data[1] status_text = event_data[2] if len(event_data) > 2 else "" if display.tft and ui_screen == UI_SCREEN_RESULT: - render_result_screen(display, current_status, current_prompt, image_generation_done) + display.render_result_screen(current_status, current_prompt, image_generation_done) elif event_data[0] == "prompt": current_prompt = event_data[1] if display.tft and ui_screen == UI_SCREEN_RESULT: - render_result_screen(display, current_status, current_prompt, image_generation_done) + display.render_result_screen(current_status, current_prompt, image_generation_done) elif event_data[0] == "image_done": image_generation_done = True if display.tft and ui_screen == UI_SCREEN_RESULT: - render_result_screen(display, "COMPLETE", current_prompt, True) + display.render_result_screen("COMPLETE", current_prompt, True) elif event_data[0] == "error": if display.tft and ui_screen == UI_SCREEN_RESULT: - render_result_screen(display, "ERROR", current_prompt, False) + display.render_result_screen("ERROR", current_prompt, False) except Exception as e: print(f"WS Recv Error: {e}")