Build Pigway Frame Flow with bilingual UI, complete KoKonna driver and safe automation
This commit is contained in:
Vendored
+243
@@ -0,0 +1,243 @@
|
||||
"""
|
||||
KoKonna 画框控制库
|
||||
|
||||
用法:
|
||||
from kokonna import KoKonnaFrame
|
||||
|
||||
frame = KoKonnaFrame(device="living_room")
|
||||
frame.upload_image("/path/to/image.jpg")
|
||||
info = frame.get_device_info()
|
||||
"""
|
||||
|
||||
import base64
|
||||
import os
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
import requests
|
||||
from PIL import Image
|
||||
|
||||
# 默认 API 地址
|
||||
DEFAULT_API_BASE_URL = "https://api.galaxyguide.cn/openapi"
|
||||
|
||||
# 配置文件路径
|
||||
CONFIG_PATHS = [
|
||||
Path.home() / ".openclaw" / "skills" / "kokonna-frame" / "config.yaml",
|
||||
Path.home() / ".openclaw" / "skills" / "kokonna-frame" / "config.yml",
|
||||
Path.cwd() / "config.yaml",
|
||||
]
|
||||
|
||||
|
||||
def load_config() -> Dict[str, Any]:
|
||||
"""从配置文件加载配置"""
|
||||
import yaml
|
||||
|
||||
for config_path in CONFIG_PATHS:
|
||||
if config_path.exists():
|
||||
with open(config_path, 'r', encoding='utf-8') as f:
|
||||
return yaml.safe_load(f) or {}
|
||||
|
||||
# 没有配置文件,返回空配置
|
||||
return {}
|
||||
|
||||
|
||||
def get_api_base_url() -> str:
|
||||
"""获取 API Base URL"""
|
||||
config = load_config()
|
||||
return config.get("api_base_url", DEFAULT_API_BASE_URL)
|
||||
|
||||
|
||||
def get_device_config(device_name: str) -> Dict[str, Any]:
|
||||
"""获取指定设备的配置"""
|
||||
config = load_config()
|
||||
devices = config.get("devices", {})
|
||||
|
||||
if device_name not in devices:
|
||||
available = list(devices.keys())
|
||||
if available:
|
||||
raise ValueError(f"未知设备: {device_name},可用设备: {available}")
|
||||
else:
|
||||
raise ValueError(f"未知设备: {device_name},未配置任何设备。请在 config.yaml 中添加设备配置。")
|
||||
|
||||
device_config = devices[device_name]
|
||||
|
||||
# device_config 应该是 API Key 字符串,或者是包含详细配置的字典
|
||||
if isinstance(device_config, str):
|
||||
return {"api_key": device_config}
|
||||
else:
|
||||
return device_config
|
||||
|
||||
|
||||
def generate_filename(image_source=None, name_hint: str = None) -> str:
|
||||
"""生成有意义的文件名"""
|
||||
from datetime import datetime
|
||||
import re
|
||||
|
||||
if name_hint:
|
||||
# 如果提供了名称提示,使用它
|
||||
# 清理文件名:只保留中文、英文、数字、下划线、连字符
|
||||
clean_name = re.sub(r'[^\w\u4e00-\u9fff\-]', '_', name_hint)
|
||||
clean_name = re.sub(r'_+', '_', clean_name).strip('_')
|
||||
if clean_name:
|
||||
return f"{clean_name}.jpg"
|
||||
|
||||
# 从原文件名提取有意义部分
|
||||
if image_source and isinstance(image_source, (str, Path)):
|
||||
basename = Path(image_source).name
|
||||
name_without_ext = Path(image_source).stem
|
||||
# 去掉时间戳类的后缀
|
||||
clean_name = re.sub(r'[_\-]?\d{6,}[_\-]?\d*$', '', name_without_ext)
|
||||
clean_name = re.sub(r'[_\-]?\d{10,}', '', clean_name)
|
||||
if clean_name and len(clean_name) > 2:
|
||||
return f"{clean_name}.jpg"
|
||||
|
||||
# 默认使用时间戳
|
||||
return datetime.now().strftime("%Y%m%d_%H%M%S") + ".jpg"
|
||||
|
||||
|
||||
class KoKonnaFrame:
|
||||
"""KoKonna 画框控制类"""
|
||||
|
||||
def __init__(self, device: str, width: int = None, height: int = None):
|
||||
"""
|
||||
初始化画框
|
||||
|
||||
Args:
|
||||
device: 设备名称(在 config.yaml 中配置)
|
||||
width: 画框宽度(可选,如未配置会自动从设备获取)
|
||||
height: 画框高度(可选,如未配置会自动从设备获取)
|
||||
"""
|
||||
self.device_key = device
|
||||
self.api_base_url = get_api_base_url()
|
||||
device_config = get_device_config(device)
|
||||
|
||||
self.api_key = device_config.get("api_key")
|
||||
if not self.api_key:
|
||||
raise ValueError(f"设备 {device} 未配置 api_key")
|
||||
|
||||
self.width = width or device_config.get("width")
|
||||
self.height = height or device_config.get("height")
|
||||
|
||||
# 如果没有配置尺寸,尝试从设备获取
|
||||
if not self.width or not self.height:
|
||||
info = self.get_device_info()
|
||||
if info:
|
||||
# 根据旋转角度确定实际显示尺寸
|
||||
rotation = info.get("rotation", 0)
|
||||
screen_width = info.get("screen_width", 480)
|
||||
screen_height = info.get("screen_height", 800)
|
||||
|
||||
if rotation in [90, 270]:
|
||||
self.width = screen_height
|
||||
self.height = screen_width
|
||||
else:
|
||||
self.width = screen_width
|
||||
self.height = screen_height
|
||||
|
||||
def _resize_image(self, image_source) -> bytes:
|
||||
"""调整图片尺寸"""
|
||||
if isinstance(image_source, (str, Path)):
|
||||
img = Image.open(image_source)
|
||||
else:
|
||||
img = Image.open(image_source)
|
||||
|
||||
# 转换颜色模式
|
||||
if img.mode == "RGBA":
|
||||
background = Image.new("RGB", img.size, (255, 255, 255))
|
||||
background.paste(img, mask=img.split()[3])
|
||||
img = background
|
||||
elif img.mode != "RGB":
|
||||
img = img.convert("RGB")
|
||||
|
||||
# 如果没有尺寸信息,直接返回原图
|
||||
if not self.width or not self.height:
|
||||
buffer = BytesIO()
|
||||
img.save(buffer, format="JPEG", quality=95)
|
||||
return buffer.getvalue()
|
||||
|
||||
# 裁剪到目标比例
|
||||
target_ratio = self.width / self.height
|
||||
w, h = img.size
|
||||
current_ratio = w / h
|
||||
|
||||
if current_ratio > target_ratio:
|
||||
# 图片太宽,裁剪两边
|
||||
new_w = int(h * target_ratio)
|
||||
left = (w - new_w) // 2
|
||||
img = img.crop((left, 0, left + new_w, h))
|
||||
elif current_ratio < target_ratio:
|
||||
# 图片太高,裁剪上下
|
||||
new_h = int(w / target_ratio)
|
||||
top = (h - new_h) // 2
|
||||
img = img.crop((0, top, w, top + new_h))
|
||||
|
||||
# 缩放到目标尺寸
|
||||
img = img.resize((self.width, self.height), Image.LANCZOS)
|
||||
|
||||
# 保存为 JPEG
|
||||
buffer = BytesIO()
|
||||
img.save(buffer, format="JPEG", quality=95)
|
||||
return buffer.getvalue()
|
||||
|
||||
def get_device_info(self) -> Optional[Dict]:
|
||||
"""获取设备信息"""
|
||||
url = f"{self.api_base_url}/device"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(url, headers=headers, json={}, timeout=10)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
print(f"获取设备信息失败: {e}")
|
||||
return None
|
||||
|
||||
def upload_image(self, image_source, name: str = None) -> Dict:
|
||||
"""
|
||||
上传图片到画框
|
||||
|
||||
Args:
|
||||
image_source: 图片路径或文件对象
|
||||
name: 图片名称(可选)
|
||||
|
||||
Returns:
|
||||
API 响应
|
||||
"""
|
||||
# 生成文件名
|
||||
filename = generate_filename(image_source, name)
|
||||
|
||||
# 处理图片
|
||||
image_data = self._resize_image(image_source)
|
||||
base64_data = base64.b64encode(image_data).decode('utf-8')
|
||||
|
||||
# 上传
|
||||
url = f"{self.api_base_url}/upload"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
payload = {
|
||||
"base64": base64_data,
|
||||
"name": filename,
|
||||
}
|
||||
|
||||
response = requests.post(url, headers=headers, json=payload, timeout=30)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
|
||||
|
||||
# 便捷函数
|
||||
def upload_to_device(device: str, image_path: str, name: str = None) -> Dict:
|
||||
"""上传图片到指定设备"""
|
||||
frame = KoKonnaFrame(device=device)
|
||||
return frame.upload_image(image_path, name=name)
|
||||
|
||||
|
||||
def get_device_info(device: str) -> Optional[Dict]:
|
||||
"""获取指定设备信息"""
|
||||
frame = KoKonnaFrame(device=device)
|
||||
return frame.get_device_info()
|
||||
Reference in New Issue
Block a user