diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 99550f8..036e34c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,8 +14,8 @@ repos: require_serial: true additional_dependencies: [] - - id: ruff - name: ruff + - id: ruff-check + name: ruff-check description: "Run 'ruff' for extremely fast Python linting" entry: uv run --dev ruff check pass_filenames: false diff --git a/.ruff.toml b/.ruff.toml index 24b0d59..4d913b0 100644 --- a/.ruff.toml +++ b/.ruff.toml @@ -1,3 +1,4 @@ +target-version = "py310" exclude = ["docs/**"] line-length = 120 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 28f8fc8..0c8e933 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,7 @@ We welcome contributions! These guidelines exist to save everyone time. Followin ## Development Setup -1. Make sure you have `Python 3.12+` installed. +1. Make sure you have `Python 3.10+` installed. 1. Install [uv](https://docs.astral.sh/uv/getting-started/installation/). 1. Fork the repository and clone your fork. 1. Install development dependencies: `make prepare`. diff --git a/examples/jetarm_demo/connect_pychannel_with_rcply.py b/examples/jetarm_demo/connect_pychannel_with_rcply.py index 14baef0..6684e63 100644 --- a/examples/jetarm_demo/connect_pychannel_with_rcply.py +++ b/examples/jetarm_demo/connect_pychannel_with_rcply.py @@ -1,5 +1,5 @@ -import asyncio import argparse +import asyncio from ghoshell_moss.transports.zmq_channel.zmq_channel import ZMQChannelProxy diff --git a/examples/jetarm_demo/jetarm_agent.py b/examples/jetarm_demo/jetarm_agent.py index bac854a..1daf043 100644 --- a/examples/jetarm_demo/jetarm_agent.py +++ b/examples/jetarm_demo/jetarm_agent.py @@ -1,7 +1,8 @@ +import argparse import asyncio +from pathlib import Path from ghoshell_container import Container -import argparse from ghoshell_moss.core.shell import new_shell from ghoshell_moss.speech import make_baseline_tts_speech @@ -10,8 +11,7 @@ from ghoshell_moss.transports.zmq_channel.zmq_channel import ZMQChannelProxy from ghoshell_moss_contrib.agent import ModelConf, SimpleAgent from ghoshell_moss_contrib.agent.chat import ConsoleChat -from ghoshell_moss_contrib.example_ws import workspace_container, get_container -from pathlib import Path +from ghoshell_moss_contrib.example_ws import get_container, workspace_container ADDRESS = "tcp://192.168.1.15:9527" """填入正确的 ip, 需要先对齐 jetarm_ws 运行的机器设备和监听的端口. """ diff --git a/examples/miku/main.py b/examples/miku/main.py index e5885b5..6b3b0dc 100644 --- a/examples/miku/main.py +++ b/examples/miku/main.py @@ -1,25 +1,25 @@ +import asyncio +import importlib.util import os import sys - -from ghoshell_moss.speech import make_baseline_tts_speech, Speech -from ghoshell_moss.speech.player.pyaudio_player import PyAudioStreamPlayer -from ghoshell_moss.speech.volcengine_tts import VolcengineTTS, VolcengineTTSConf -from ghoshell_moss_contrib.agent import ModelConf, SimpleAgent - -import asyncio from os.path import dirname, join import live2d.v3 as live2d import pygame from ghoshell_container import Container +from ghoshell_moss.speech import Speech, make_baseline_tts_speech +from ghoshell_moss.speech.player.pyaudio_player import PyAudioStreamPlayer +from ghoshell_moss.speech.volcengine_tts import VolcengineTTS, VolcengineTTSConf +from ghoshell_moss_contrib.agent import ModelConf, SimpleAgent + current_dir = os.path.dirname(os.path.abspath(__file__)) -try: - import miku_channels -except ImportError: +if importlib.util.find_spec(miku_channels) is None: # 加载当前路径. sys.path.append(current_dir) +import pathlib + from miku_channels.arm import left_arm_chan, right_arm_chan from miku_channels.body import body_chan from miku_channels.elbow import left_elbow_chan, right_elbow_chan @@ -30,9 +30,9 @@ from miku_channels.leg import left_leg_chan, right_leg_chan from miku_channels.necktie import necktie_chan from miku_provider import init_live2d, init_pygame + from ghoshell_moss.core.shell import new_shell -from ghoshell_moss_contrib.example_ws import workspace_container, get_example_speech -import pathlib +from ghoshell_moss_contrib.example_ws import get_example_speech, workspace_container # 全局状态 WIDTH = 600 diff --git a/examples/miku/miku_provider.py b/examples/miku/miku_provider.py index 52fdbb2..1521573 100644 --- a/examples/miku/miku_provider.py +++ b/examples/miku/miku_provider.py @@ -1,4 +1,5 @@ import asyncio +import importlib.util import os import sys from os.path import dirname, join @@ -7,10 +8,8 @@ import pygame from ghoshell_container import Container, get_container -try: - import miku_channels -except ImportError: - current_dir = os.path.dirname(os.path.abspath(__file__)) +current_dir = os.path.dirname(os.path.abspath(__file__)) +if importlib.util.find_spec(miku_channels) is None: sys.path.append(current_dir) from miku_channels.arm import left_arm_chan, right_arm_chan @@ -22,8 +21,9 @@ from miku_channels.head import head_chan from miku_channels.leg import left_leg_chan, right_leg_chan from miku_channels.necktie import necktie_chan -from ghoshell_moss.transports.zmq_channel import ZMQChannelProvider + from ghoshell_moss import Channel +from ghoshell_moss.transports.zmq_channel import ZMQChannelProvider # 全局状态 model: live2d.LAppModel | None = None diff --git a/examples/moss_agent.py b/examples/moss_agent.py index b7de7a1..83fb945 100644 --- a/examples/moss_agent.py +++ b/examples/moss_agent.py @@ -1,22 +1,17 @@ -import os.path -import pathlib import asyncio +import pathlib -from ghoshell_common.contracts import Workspace, LoggerItf +from ghoshell_common.contracts import LoggerItf, Workspace from ghoshell_container import Container -from ghoshell_moss_contrib.example_ws import workspace_container, get_example_speech -from ghoshell_moss.channels.mac_channel import new_mac_control_channel -from ghoshell_moss_contrib.channels.mermaid_draw import new_mermaid_chan -from ghoshell_moss_contrib.channels.web_bookmark import build_web_bookmark_chan - -from ghoshell_moss_contrib.agent import SimpleAgent, ModelConf, ConsoleChat from ghoshell_moss.core.shell import new_shell -from ghoshell_moss.transports.zmq_channel.zmq_hub import ZMQChannelHub, ZMQHubConfig, ZMQProxyConfig # 不着急删除, 方便自测时开启. -from ghoshell_moss_contrib.channels.screen_capture import ScreenCapture -from ghoshell_moss.transports.zmq_channel.zmq_hub import ZMQChannelProxy +from ghoshell_moss.transports.zmq_channel.zmq_hub import ZMQChannelHub, ZMQHubConfig, ZMQProxyConfig +from ghoshell_moss_contrib.agent import ConsoleChat, ModelConf, SimpleAgent +from ghoshell_moss_contrib.channels.mermaid_draw import new_mermaid_chan +from ghoshell_moss_contrib.channels.web_bookmark import build_web_bookmark_chan +from ghoshell_moss_contrib.example_ws import get_example_speech, workspace_container """ 说明: @@ -118,11 +113,11 @@ def run_moss_agent(container: Container): instruction=instructions, chat=ConsoleChat(logger=logger), model=ModelConf( - kwargs=dict( - thinking=dict( - type="disabled", - ) - ), + kwargs={ + "thinking": { + "type": "disabled", + }, + }, ), shell=shell, ) diff --git a/examples/moss_zmq_channels/miku_app.py b/examples/moss_zmq_channels/miku_app.py index ff82c35..6b06105 100644 --- a/examples/moss_zmq_channels/miku_app.py +++ b/examples/moss_zmq_channels/miku_app.py @@ -1,20 +1,19 @@ -from os.path import dirname, join import sys +from os.path import dirname, join # patch miku 的读取路径. # 由于 miku 还是一个实验性的数字人项目, 暂时不希望把它打包到 ghoshell_moss_contrib 里 (太大) # 所以先用比较脏的相对路径来读取它. current_dir = dirname(__file__) workspace_dir = join(dirname(current_dir), ".workspace") -try: - import miku -except ImportError: - miku_dir = join(dirname(current_dir), "miku") - print(miku_dir) +miku_dir = join(dirname(current_dir), "miku") +if miku_dir not in sys.path: sys.path.append(miku_dir) - from miku_provider import run_game_with_zmq_provider import asyncio + +from miku_provider import run_game_with_zmq_provider + from ghoshell_moss_contrib.example_ws import workspace_container if __name__ == "__main__": diff --git a/examples/moss_zmq_channels/vision_app.py b/examples/moss_zmq_channels/vision_app.py index 1b4e9af..3fa4758 100644 --- a/examples/moss_zmq_channels/vision_app.py +++ b/examples/moss_zmq_channels/vision_app.py @@ -1,6 +1,6 @@ -from ghoshell_moss_contrib.channels.opencv_vision import OpenCVVision from ghoshell_moss import get_container from ghoshell_moss.transports.zmq_channel import ZMQChannelProvider +from ghoshell_moss_contrib.channels.opencv_vision import OpenCVVision if __name__ == "__main__": # 初始化容器 diff --git a/examples/vision_exam/vision_provider.py b/examples/vision_exam/vision_provider.py index 1b4e9af..3fa4758 100644 --- a/examples/vision_exam/vision_provider.py +++ b/examples/vision_exam/vision_provider.py @@ -1,6 +1,6 @@ -from ghoshell_moss_contrib.channels.opencv_vision import OpenCVVision from ghoshell_moss import get_container from ghoshell_moss.transports.zmq_channel import ZMQChannelProvider +from ghoshell_moss_contrib.channels.opencv_vision import OpenCVVision if __name__ == "__main__": # 初始化容器 diff --git a/examples/vision_exam/vision_proxy.py b/examples/vision_exam/vision_proxy.py index 1477471..0c6e948 100644 --- a/examples/vision_exam/vision_proxy.py +++ b/examples/vision_exam/vision_proxy.py @@ -1,8 +1,8 @@ import asyncio +from ghoshell_moss.message.contents import Base64Image from ghoshell_moss.transports.zmq_channel.zmq_channel import ZMQChannelProxy from ghoshell_moss_contrib.gui.image_viewer import SimpleImageViewer, run_img_viewer -from ghoshell_moss.message.contents import Base64Image if __name__ == "__main__": # 测试专用. diff --git a/src/ghoshell_moss/core/concepts/command.py b/src/ghoshell_moss/core/concepts/command.py index 5bd7368..43fe0b8 100644 --- a/src/ghoshell_moss/core/concepts/command.py +++ b/src/ghoshell_moss/core/concepts/command.py @@ -62,11 +62,11 @@ class CommandTaskStateType(str, Enum): @classmethod def is_complete(cls, state: str | Self) -> bool: - return state == cls.done or state == cls.failed or state == cls.cancelled + return state in (cls.done, cls.failed, cls.cancelled) @classmethod def is_stopped(cls, state: str | Self) -> bool: - return state == cls.cancelled or state == cls.failed + return state in (cls.cancelled, cls.failed) class CommandTaskState(str, Enum): diff --git a/src/ghoshell_moss/speech/__init__.py b/src/ghoshell_moss/speech/__init__.py index 27eaaa6..222aa9d 100644 --- a/src/ghoshell_moss/speech/__init__.py +++ b/src/ghoshell_moss/speech/__init__.py @@ -1,6 +1,6 @@ from ghoshell_common.contracts import LoggerItf -from ghoshell_moss.core.concepts.speech import TTS, StreamAudioPlayer, Speech, SpeechStream +from ghoshell_moss.core.concepts.speech import TTS, Speech, SpeechStream, StreamAudioPlayer from ghoshell_moss.speech.mock import MockSpeech from ghoshell_moss.speech.stream_tts_speech import TTSSpeech, TTSSpeechStream diff --git a/src/ghoshell_moss/speech/mock.py b/src/ghoshell_moss/speech/mock.py index 41ad1c0..5be462f 100644 --- a/src/ghoshell_moss/speech/mock.py +++ b/src/ghoshell_moss/speech/mock.py @@ -1,4 +1,5 @@ import threading +import time from queue import Empty, Queue from typing import Optional @@ -6,7 +7,6 @@ from ghoshell_moss.core.concepts.speech import Speech, SpeechStream from ghoshell_moss.core.helpers.asyncio_utils import ThreadSafeEvent -import time class MockSpeechStream(SpeechStream): diff --git a/src/ghoshell_moss_contrib/channels/opencv_vision.py b/src/ghoshell_moss_contrib/channels/opencv_vision.py index 0c1826d..be6dc4e 100644 --- a/src/ghoshell_moss_contrib/channels/opencv_vision.py +++ b/src/ghoshell_moss_contrib/channels/opencv_vision.py @@ -1,16 +1,16 @@ +import logging import threading import time from datetime import datetime -from typing import List, Optional, Tuple -from PIL import Image -import cv2 +from typing import Optional -from ghoshell_moss.message import Message, Base64Image, Text +import cv2 from ghoshell_common.contracts import LoggerItf from ghoshell_container import IoCContainer, get_container +from PIL import Image + from ghoshell_moss import PyChannel -from ghoshell_moss.transports.zmq_channel.zmq_channel import ZMQChannelProvider -import logging +from ghoshell_moss.message import Base64Image, Message, Text class OpenCVVision: @@ -58,7 +58,7 @@ def _initialize_camera(self) -> bool: # 测试读取一帧 ret, _ = self._cap.read() if ret: - self._logger.info(f"摄像头初始化成功,使用索引 {camera_index}") + self._logger.info("摄像头初始化成功,使用索引 %s", camera_index) return True else: self._cap.release() @@ -66,8 +66,8 @@ def _initialize_camera(self) -> bool: self._logger.error("无法初始化任何摄像头") return False - except Exception as e: - self._logger.error(f"摄像头初始化失败: {e}") + except Exception: + self._logger.exception("摄像头初始化失败") return False def _capture_frame(self) -> bool: @@ -152,15 +152,15 @@ def close(self) -> None: # 确保窗口被销毁 try: cv2.destroyWindow(self._window_name) - except: - pass + except Exception: + self._logger.debug("销毁 OpenCV 窗口失败: %s", self._window_name, exc_info=True) # 最后调用 destroyAllWindows 确保清理所有窗口 cv2.destroyAllWindows() self._logger.info("视觉模块已关闭") - def get_cached_image(self) -> Tuple[Optional[Image.Image], float]: + def get_cached_image(self) -> tuple[Optional[Image.Image], float]: """ 线程安全地获取缓存的图像和时间戳 @@ -194,7 +194,7 @@ async def start_looking(self) -> None: self._is_caching_image = True self._last_capture_time = 0.0 # 重置时间,立即捕获下一帧 - async def context_messages(self) -> List[Message]: + async def context_messages(self) -> list[Message]: """ 返回最新的视觉信息作为上下文消息 @@ -301,7 +301,7 @@ def run_opencv_loop(self) -> None: except KeyboardInterrupt: self._logger.info("收到键盘中断信号") - except Exception as e: - self._logger.error(f"视觉模块运行异常: {e}") + except Exception: + self._logger.exception("视觉模块运行异常") finally: self.close() diff --git a/src/ghoshell_moss_contrib/example_ws.py b/src/ghoshell_moss_contrib/example_ws.py index b1e8da3..1afa7c1 100644 --- a/src/ghoshell_moss_contrib/example_ws.py +++ b/src/ghoshell_moss_contrib/example_ws.py @@ -1,19 +1,20 @@ +import logging import os +from contextlib import contextmanager +from pathlib import Path from typing import List -from ghoshell_container import Provider, Container, set_container, get_container -from ghoshell_common.contracts import LocalWorkspaceProvider, LoggerItf, WorkspaceConfigsProvider, Workspace +from ghoshell_common.contracts import LocalWorkspaceProvider, LoggerItf, WorkspaceConfigsProvider +from ghoshell_container import Container, Provider, get_container, set_container + from ghoshell_moss.core import Speech -from pathlib import Path -from contextlib import contextmanager -import logging __all__ = [ "get_container", - "set_container", + "get_example_speech", "init_container", + "set_container", "workspace_container", - "get_example_speech", ] diff --git a/src/ghoshell_moss_contrib/gui/image_viewer.py b/src/ghoshell_moss_contrib/gui/image_viewer.py index d08744b..b5843ea 100644 --- a/src/ghoshell_moss_contrib/gui/image_viewer.py +++ b/src/ghoshell_moss_contrib/gui/image_viewer.py @@ -1,10 +1,11 @@ import sys import threading -from typing import Callable +from collections.abc import Callable + +from PIL import Image +from PyQt6.QtCore import QObject, Qt, pyqtSignal +from PyQt6.QtGui import QImage, QPixmap from PyQt6.QtWidgets import QApplication, QLabel, QMainWindow -from PyQt6.QtGui import QPixmap, QImage -from PyQt6.QtCore import Qt, pyqtSignal, QObject -from PIL import Image, ImageDraw __all__ = ["SimpleImageViewer", "run_img_viewer"] @@ -46,7 +47,7 @@ def _update_pixmap(self): ) self.label.setPixmap(scaled) - def resizeEvent(self, event): + def resizeEvent(self, event): # noqa: N802 self._update_pixmap() super().resizeEvent(event)