Compare commits

...

18 Commits

Author SHA1 Message Date
jeremygan2021
e515395b55 action
All checks were successful
Deploy to Server / deploy (push) Successful in 4m27s
2026-03-02 15:37:39 +08:00
jeremygan2021
59f174322a action
All checks were successful
Deploy to Server / deploy (push) Successful in 1s
2026-03-02 15:33:04 +08:00
jeremygan2021
11e4de9071 action
Some checks failed
Deploy to Server / deploy (push) Has been cancelled
2026-03-02 15:32:20 +08:00
jeremygan2021
75cfb27bb1 action
Some checks failed
Deploy to Server / deploy (push) Has been cancelled
2026-03-02 15:30:46 +08:00
jeremygan2021
f91fabad0c action
Some checks failed
Deploy to Server / deploy (push) Failing after 2s
2026-03-02 15:29:38 +08:00
jeremygan2021
ae2da39496 action
Some checks failed
Deploy to Server / deploy (push) Failing after 2s
2026-03-02 15:26:43 +08:00
jeremygan2021
bb814061e7 action
Some checks failed
Deploy to Server / deploy (push) Has been cancelled
2026-03-02 15:25:42 +08:00
jeremygan2021
d74eb795c3 action
Some checks failed
Deploy to Server / deploy (push) Failing after 3s
2026-03-02 14:27:01 +08:00
jeremygan2021
8b8e1d51ce action
Some checks failed
Deploy to Server / deploy (push) Failing after 8s
2026-03-02 13:14:19 +08:00
jeremygan2021
4a36952484 action
Some checks failed
Deploy to Server / deploy (push) Failing after 0s
2026-03-02 13:11:22 +08:00
jeremygan2021
8b672d026d action
Some checks failed
Deploy to Server / deploy (push) Failing after 0s
2026-03-02 13:10:36 +08:00
jeremygan2021
841bf23d4d action
Some checks failed
Deploy to Server / deploy (push) Failing after 6s
2026-03-02 13:09:17 +08:00
jeremygan2021
373ce8cb2e action
Some checks failed
Deploy to Server / deploy (push) Has been cancelled
2026-03-02 13:07:34 +08:00
jeremygan2021
a1e8c042ca action
Some checks failed
Deploy to Server / deploy (push) Has been cancelled
2026-03-02 13:01:04 +08:00
jeremygan2021
0d140cd75c action 2026-03-02 12:59:46 +08:00
jeremygan2021
9620a4138d AI生图 2026-03-02 12:53:36 +08:00
jeremygan2021
37b2cf6ba6 AI图片 2026-03-02 12:32:53 +08:00
jeremygan2021
82bba110ee AI图片 2026-03-02 12:32:45 +08:00
34 changed files with 520 additions and 22 deletions

4
.env
View File

@@ -1,9 +1,11 @@
# 环境变量配置文件
# 数据库配置
# DATABASE_URL=postgresql://luna:123luna@121.43.104.161:6432/luna
# DATABASE_URL=postgresql://luna:123luna@121.43.104.161:6433/luna
DATABASE_URL=postgresql://luna:123luna@6.6.6.66:5432/luna
DASHSCOPE_API_KEY=sk-a294f382488d46a1aa0d7cd8e750729b
# MQTT配置
MQTT_BROKER_HOST=luna-mqtt
MQTT_BROKER_PORT=1883

View File

@@ -1,7 +1,7 @@
# 环境变量配置文件
# 数据库配置
# DATABASE_URL=postgresql://luna:123luna@121.43.104.161:6432/luna
# DATABASE_URL=postgresql://luna:123luna@121.43.104.161:6433/luna
DATABASE_URL=postgresql://luna:123luna@6.6.6.66:5432/luna
# MQTT配置
MQTT_BROKER_HOST=luna-mqtt
@@ -9,6 +9,8 @@ MQTT_BROKER_PORT=1883
MQTT_USERNAME=luna2025
MQTT_PASSWORD=123luna2021
DASHSCOPE_API_KEY=sk-a294f382488d46a1aa0d7cd8e750729b
# 应用配置
APP_NAME=墨水屏桌面屏幕系统
DEBUG=false

View File

@@ -1,7 +1,7 @@
# 环境变量配置文件
# 数据库配置
# DATABASE_URL=postgresql://luna:123luna@121.43.104.161:6432/luna
# DATABASE_URL=postgresql://luna:123luna@121.43.104.161:6433/luna
DATABASE_URL=postgresql://luna:123luna@6.6.6.66:5432/luna
# MQTT配置
MQTT_BROKER_HOST=luna-mqtt
@@ -9,6 +9,10 @@ MQTT_BROKER_PORT=1883
MQTT_USERNAME=luna2025
MQTT_PASSWORD=123luna2021
DASHSCOPE_API_KEY=sk-a294f382488d46a1aa0d7cd*******
# 应用配置
APP_NAME=墨水屏桌面屏幕系统
DEBUG=false

View File

@@ -0,0 +1,147 @@
name: Deploy to Server
on:
push:
branches:
- main
- master
jobs:
deploy:
runs-on: ubuntu
steps:
# 直接使用 expect 脚本处理交互,比 sshpass 更稳定,尤其是在 Alpine 上
- name: Install dependencies
run: |
if command -v apt-get &> /dev/null; then
apt-get update
apt-get install -y expect openssh-client
elif command -v apk &> /dev/null; then
apk update
apk add expect openssh-client bash
else
echo "Unknown package manager"
exit 1
fi
- name: Deploy to server
env:
HOST: "6.6.6.66"
USER: "quant"
PASS: "123quant-speed"
TARGET_DIR: "/home/quant-speed/data/dev/ESP32_GDEY042T81_server"
REPO_URL: "https://gitea.tangledup-ai.com/Tangledup-ai/ESP32_GDEY042T81_server.git"
run: |
# 创建一个 shell 脚本,包含所有需要在服务器上执行的逻辑
# 这样可以避免在 expect 中处理复杂的条件判断和转义
cat > remote_script.sh <<'EOS'
#!/bin/bash
# 获取密码
PASSWORD="$1"
TARGET_DIR="$2"
RUN_USER="$3"
REPO_URL="$4"
# 1. 确保目录存在 (脚本已通过 sudo 运行)
if [ ! -d "$TARGET_DIR" ]; then
mkdir -p "$TARGET_DIR"
fi
# 2. 修正权限,确保用户拥有目录
chown -R "$RUN_USER:$RUN_USER" "$TARGET_DIR"
# 3. 进入目录
cd "$TARGET_DIR"
# 4. 执行 Git 操作 (以用户身份执行,避免 .git 权限问题)
# 使用 sudo -u 切换到普通用户执行 git
echo "Deploying code as $RUN_USER..."
FRESH_CLONE=0
if [ ! -d ".git" ]; then
echo "Not a git repository. Cloning from $REPO_URL..."
# Handle non-empty dir if necessary
if [ "$(ls -A)" ]; then
echo "Directory not empty. initializing git..."
sudo -u "$RUN_USER" git init
sudo -u "$RUN_USER" git remote add origin "$REPO_URL"
sudo -u "$RUN_USER" git fetch origin
sudo -u "$RUN_USER" git checkout -b main --track origin/main || sudo -u "$RUN_USER" git reset --hard origin/main
else
sudo -u "$RUN_USER" git clone "$REPO_URL" .
fi
FRESH_CLONE=1
fi
if [ "$FRESH_CLONE" -eq 0 ]; then
OLD_HEAD=$(sudo -u "$RUN_USER" git rev-parse HEAD 2>/dev/null || echo "")
echo "Pulling latest code..."
if ! sudo -u "$RUN_USER" git pull origin main; then
echo "Git pull failed"
exit 1
fi
NEW_HEAD=$(sudo -u "$RUN_USER" git rev-parse HEAD)
if [ "$OLD_HEAD" == "$NEW_HEAD" ]; then
echo "No changes detected, skipping deploy"
exit 0
fi
else
OLD_HEAD=""
NEW_HEAD=$(sudo -u "$RUN_USER" git rev-parse HEAD)
fi
# 5. 执行 Docker 操作 (以 root 身份执行)
# 检查构建文件变动
FORCE_REBUILD=0
if [ "$FRESH_CLONE" -eq 1 ]; then
FORCE_REBUILD=1
fi
if [ "$FORCE_REBUILD" -eq 1 ] || sudo -u "$RUN_USER" git diff --name-only $OLD_HEAD $NEW_HEAD | grep -E 'Dockerfile|requirements.txt'; then
echo "Build files changed or fresh clone, rebuilding..."
docker compose down --rmi local
docker rmi epaper_server:latest || true
docker compose up -d --build
else
echo "Only code changed, restarting container..."
docker compose down
docker compose up -d
fi
EOS
# 创建 expect 脚本,负责上传到 /tmp 并 sudo 执行
cat > deploy_script.exp <<EOF
#!/usr/bin/expect -f
set timeout 300
set host "$HOST"
set user "$USER"
set password "$PASS"
set target_dir "$TARGET_DIR"
set repo_url "$REPO_URL"
# 1. 上传脚本到 /tmp (避免目标目录权限问题)
spawn scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null remote_script.sh \$user@\$host:/tmp/luna_deploy.sh
expect {
"yes/no" { send "yes\r"; exp_continue }
"password:" { send "\$password\r" }
}
expect eof
# 2. SSH 登录并执行 sudo bash /tmp/luna_deploy.sh
# 我们把密码传给脚本,让脚本内部决定怎么用,或者直接用 sudo 执行脚本
spawn ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -t \$user@\$host "echo '\$password' | sudo -S bash /tmp/luna_deploy.sh '\$password' '\$target_dir' '\$user' '\$repo_url'"
expect {
"password:" { send "\$password\r" }
}
# 保持交互直到脚本执行完毕
expect eof
EOF
# 执行 expect 脚本
chmod +x deploy_script.exp
./deploy_script.exp

View File

@@ -1,7 +1,7 @@
使用fastAPI框架
python 3.12
- 使用pg数据库 url http://121.43.104.161:6432
- 使用pg数据库 url http://121.43.104.161:6433
- 用户名luna
- 密码123luna

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,9 +1,10 @@
from fastapi import APIRouter
from api import devices, contents, todos
from api import devices, contents, todos, ai
api_router = APIRouter()
# 注册所有路由,并添加全局安全要求
api_router.include_router(devices.router, prefix="/devices")
api_router.include_router(contents.router, prefix="/contents")
api_router.include_router(todos.router, prefix="/todos")
api_router.include_router(todos.router, prefix="/todos")
api_router.include_router(ai.router, prefix="/ai", tags=["AI生成"])

Binary file not shown.

Binary file not shown.

Binary file not shown.

233
api/ai.py Normal file
View File

@@ -0,0 +1,233 @@
import os
import uuid
import httpx
from fastapi import APIRouter, HTTPException, Depends
from typing import Dict, Any
import logging
from config import settings
from api.ai_schemas import AIGenerationRequest, AITaskResponse, AITaskResult, AITemplateGenerationRequest
from api.prompts import get_prompt
router = APIRouter()
logger = logging.getLogger(__name__)
DASHSCOPE_API_URL = "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation"
DASHSCOPE_TASK_URL = "https://dashscope.aliyuncs.com/api/v1/tasks"
async def _download_image(url: str) -> str:
"""
下载图片并保存到media目录
:param url: 图片URL
:return: 本地文件相对路径 (e.g., "/media/xxx.png")
"""
try:
async with httpx.AsyncClient() as client:
response = await client.get(url, timeout=30.0)
if response.status_code != 200:
logger.error(f"Failed to download image: {url}, status: {response.status_code}")
return url # 下载失败返回原URL
# 生成文件名
filename = f"{uuid.uuid4()}.png"
filepath = os.path.join(settings.media_dir, filename)
# 写入文件
with open(filepath, "wb") as f:
f.write(response.content)
# 返回可访问的URL路径
return f"/media/{filename}"
except Exception as e:
logger.error(f"Error downloading image: {str(e)}")
return url
async def _submit_dashscope_task(prompt: str, negative_prompt: str, size: str, n: int, model: str):
if not settings.dashscope_api_key:
raise HTTPException(status_code=500, detail="DashScope API Key not configured")
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {settings.dashscope_api_key}"
# "X-DashScope-Async": "enable" # 移除强制异步,因为某些账号/模型不支持
}
# 构建请求体
payload = {
"model": model,
"input": {
"messages": [
{
"role": "user",
"content": [
{
"text": prompt
}
]
}
]
},
"parameters": {
"prompt_extend": True,
"watermark": False,
"n": n,
"size": size
}
}
if negative_prompt:
payload["parameters"]["negative_prompt"] = negative_prompt
try:
async with httpx.AsyncClient() as client:
response = await client.post(
DASHSCOPE_API_URL,
headers=headers,
json=payload,
timeout=60.0 # 增加超时时间,同步请求可能较慢
)
if response.status_code != 200:
logger.error(f"DashScope API error: {response.text}")
try:
error_detail = response.json()
except:
error_detail = {"message": response.text}
raise HTTPException(status_code=response.status_code, detail=error_detail)
result = response.json()
# 检查是异步任务返回还是同步结果返回
task_id = None
if "output" in result and "task_id" in result["output"]:
task_id = result["output"]["task_id"]
elif "task_id" in result:
task_id = result["task_id"]
# 如果没有task_id检查是否有直接结果同步模式
if not task_id:
if "output" in result and "choices" in result["output"]:
# 同步返回成功
# 提取结果以便直接返回
choices = result["output"]["choices"]
results = []
for choice in choices:
msg_content = choice.get("message", {}).get("content", [])
for item in msg_content:
if "image" in item:
# 下载图片到本地
local_url = await _download_image(item["image"])
results.append({"url": local_url, "origin_url": item["image"]})
return AITaskResponse(
request_id=result.get("request_id"),
status="SUCCEEDED",
results=results
)
# 既没有task_id也没有choices可能是其他结构或错误
logger.warning(f"Unexpected response structure: {result}")
# 尝试从output.task_id再找一次
task_id = result.get("output", {}).get("task_id")
if not task_id:
# 依然找不到,报错
raise HTTPException(status_code=500, detail="Failed to retrieve task_id or results from DashScope response")
return AITaskResponse(
task_id=task_id,
request_id=result.get("request_id"),
status="PENDING" # 异步任务初始状态
)
except httpx.RequestError as e:
logger.error(f"Request error: {str(e)}")
raise HTTPException(status_code=500, detail=f"Request failed: {str(e)}")
@router.post("/generate", response_model=AITaskResponse, summary="提交AI图片生成任务")
async def generate_image(request: AIGenerationRequest):
"""
提交AI图片生成任务使用阿里云DashScope服务
"""
return await _submit_dashscope_task(
prompt=request.prompt,
negative_prompt=request.negative_prompt,
size=request.size,
n=request.n,
model=request.model
)
@router.post("/generate_from_template", response_model=AITaskResponse, summary="使用模板提交AI图片生成任务")
async def generate_image_from_template(request: AITemplateGenerationRequest):
"""
使用预设模板提交AI图片生成任务
"""
try:
prompt = get_prompt(request.template_id, **request.params)
if prompt is None:
raise HTTPException(status_code=404, detail=f"未找到模板ID: {request.template_id}")
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
return await _submit_dashscope_task(
prompt=prompt,
negative_prompt=request.negative_prompt,
size=request.size,
n=request.n,
model=request.model
)
@router.get("/tasks/{task_id}", response_model=AITaskResult, summary="查询AI任务结果")
async def get_task_result(task_id: str):
"""
查询AI生成任务的结果
"""
if not settings.dashscope_api_key:
raise HTTPException(status_code=500, detail="DashScope API Key not configured")
headers = {
"Authorization": f"Bearer {settings.dashscope_api_key}"
}
try:
async with httpx.AsyncClient() as client:
response = await client.get(
f"{DASHSCOPE_TASK_URL}/{task_id}",
headers=headers,
timeout=30.0
)
if response.status_code != 200:
logger.error(f"DashScope Task API error: {response.text}")
raise HTTPException(status_code=response.status_code, detail="Failed to fetch task status")
result = response.json()
# 构建返回结果
# DashScope 任务查询返回结构:
# {
# "request_id": "...",
# "output": {
# "task_id": "...",
# "task_status": "SUCCEEDED",
# "results": [...]
# },
# "usage": ...
# }
task_status = result.get("output", {}).get("task_status", "UNKNOWN")
return AITaskResult(
task_id=task_id,
status=task_status,
code=result.get("code"),
message=result.get("message"),
output=result.get("output"),
usage=result.get("usage")
)
except httpx.RequestError as e:
logger.error(f"Request error: {str(e)}")
raise HTTPException(status_code=500, detail=f"Request failed: {str(e)}")

33
api/ai_schemas.py Normal file
View File

@@ -0,0 +1,33 @@
from pydantic import BaseModel, Field
from typing import Optional, List, Dict, Any
# AI生成相关模型
class AIGenerationRequest(BaseModel):
prompt: str = Field(..., description="生成图片的提示词")
negative_prompt: Optional[str] = Field(None, description="反向提示词")
size: str = Field("1024*1024", description="图片尺寸")
n: int = Field(1, description="生成数量", ge=1, le=4)
model: str = Field("wan2.6-t2i", description="使用的模型")
class AITemplateGenerationRequest(BaseModel):
template_id: str = Field(..., description="提示词模板ID")
params: Dict[str, str] = Field(default_factory=dict, description="提示词参数")
negative_prompt: Optional[str] = Field(None, description="反向提示词")
size: str = Field("1024*1024", description="图片尺寸")
n: int = Field(1, description="生成数量", ge=1, le=4)
model: str = Field("wan2.6-t2i", description="使用的模型")
class AITaskResponse(BaseModel):
task_id: Optional[str] = Field(None, description="任务ID (异步任务时存在)")
request_id: Optional[str] = Field(None, description="请求ID")
status: Optional[str] = Field(None, description="任务状态")
results: Optional[List[Dict[str, Any]]] = Field(None, description="同步生成的直接结果")
# results结构: [{"url": "/media/xxx.png", "origin_url": "https://..."}]
class AITaskResult(BaseModel):
task_id: str
status: str
code: Optional[str] = None
message: Optional[str] = None
output: Optional[Dict[str, Any]] = None
usage: Optional[Dict[str, Any]] = None

31
api/prompts.py Normal file
View File

@@ -0,0 +1,31 @@
# 预设提示词模板库
# 使用Python的format语法进行参数替换例如 {keyword}
PROMPTS = {
"flower_shop": "一间有着精致窗户的花店,漂亮的木质门,摆放着{flower_type},风格为{style}",
"landscape": "宏伟的{season}自然风景,包含{element},光线为{lighting},高分辨率,写实风格",
"cyberpunk_city": "赛博朋克风格的未来城市,霓虹灯闪烁,{weather}天气,街道上有{vehicle}",
"portrait": "一张{gender}的肖像照,{expression}表情,背景是{background},专业摄影布光",
"default": "一间有着精致窗户的花店,漂亮的木质门,摆放着花朵"
}
def get_prompt(template_id: str, **kwargs) -> str:
"""
获取并格式化提示词
:param template_id: 提示词模板ID
:param kwargs: 替换参数
:return: 格式化后的提示词
"""
template = PROMPTS.get(template_id)
if not template:
return None
try:
# 使用 safe_substitute 避免参数缺失报错?
# Python的format如果缺参数会报错这里直接用format让调用者负责提供完整参数
# 或者我们可以在这里处理默认值
return template.format(**kwargs)
except KeyError as e:
# 如果缺少参数,抛出异常或返回包含未替换占位符的字符串
# 这里为了简单,如果出错,我们尽量保留原样或者报错
raise ValueError(f"缺少提示词参数: {e}")

View File

@@ -3,7 +3,7 @@ from typing import Optional
class Settings(BaseSettings):
# 数据库配置1
#database_url: str = "postgresql://luna:123luna@121.43.104.161:6432/luna"
#database_url: str = "postgresql://luna:123luna@121.43.104.161:6433/luna"
database_url: str = "postgresql://luna:123luna@6.6.6.66:5432/luna"
@@ -21,6 +21,7 @@ class Settings(BaseSettings):
static_dir: str = "static"
upload_dir: str = "static/uploads"
processed_dir: str = "static/processed"
media_dir: str = "media" # 新增媒体文件目录
# 墨水屏配置
ink_width: int = 400
@@ -34,6 +35,9 @@ class Settings(BaseSettings):
# 管理员配置
admin_username: str = "admin"
admin_password: str = "123456"
# DashScope配置
dashscope_api_key: Optional[str] = None
class Config:
env_file = ".env"

View File

@@ -1,14 +1,16 @@
services:
# 主应用服务
luna-app:
epaper_server:
image: epaper_server:latest
build:
context: .
dockerfile: Dockerfile
container_name: luna-app
container_name: epaper_server
ports:
- "8199:8199"
volumes:
- ./static:/app/static
- ./:/app
env_file:
- .env.docker
depends_on:

View File

@@ -17,9 +17,9 @@ SERVER_HOST="6.6.6.66" # 服务器IP地址
SERVER_USER="ubuntu" # 服务器用户名
SERVER_PASSWORD="qweasdzxc1" # 服务器密码
SERVER_PORT="22" # SSH端口默认22
IMAGE_NAME="epage_server" # Docker镜像名称
IMAGE_NAME="epaper_server" # Docker镜像名称
IMAGE_TAG="latest" # Docker镜像标签
CONTAINER_NAME="epage_server-container" # 容器名称
CONTAINER_NAME="epaper_server-container" # 容器名称
LOCAL_PORT="8199" # 本地端口
CONTAINER_PORT="8199" # 容器端口
TAR_FILE="${IMAGE_NAME}-${IMAGE_TAG}.tar" # 压缩包文件名

61
main.py
View File

@@ -1,6 +1,7 @@
from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles
from fastapi.middleware.cors import CORSMiddleware
from fastapi.openapi.utils import get_openapi
from starlette.middleware.sessions import SessionMiddleware
from contextlib import asynccontextmanager
import logging
@@ -41,6 +42,7 @@ async def lifespan(app: FastAPI):
# 确保静态文件目录存在
os.makedirs(settings.static_dir, exist_ok=True)
os.makedirs(settings.media_dir, exist_ok=True)
yield
@@ -57,19 +59,55 @@ app = FastAPI(
description="用于管理墨水屏设备、内容和待办事项的API",
version="1.0.0",
lifespan=lifespan,
openapi_components={
"securitySchemes": {
"APIKeyHeader": {
"type": "apiKey",
"in": "header",
"name": "X-API-Key",
"description": "API Key鉴权请在下方输入正确的API Key"
}
}
},
security=[{"APIKeyHeader": []}]
)
# 自定义OpenAPI模式以显示API Key鉴权按钮
def custom_openapi():
if app.openapi_schema:
return app.openapi_schema
openapi_schema = get_openapi(
title=app.title,
version=app.version,
description=app.description,
routes=app.routes,
)
# 添加安全方案
if "components" not in openapi_schema:
openapi_schema["components"] = {}
security_scheme = {
"type": "apiKey",
"in": "header",
"name": "X-API-Key",
"description": "API Key鉴权请在下方输入正确的API Key"
}
if "securitySchemes" not in openapi_schema["components"]:
openapi_schema["components"]["securitySchemes"] = {}
openapi_schema["components"]["securitySchemes"]["APIKeyHeader"] = security_scheme
# 添加全局安全要求
if "security" not in openapi_schema:
openapi_schema["security"] = []
# 避免重复添加
has_apikey_security = False
for security_req in openapi_schema["security"]:
if "APIKeyHeader" in security_req:
has_apikey_security = True
break
if not has_apikey_security:
openapi_schema["security"].append({"APIKeyHeader": []})
app.openapi_schema = openapi_schema
return app.openapi_schema
app.openapi = custom_openapi
# 添加CORS中间件
app.add_middleware(
CORSMiddleware,
@@ -90,6 +128,7 @@ app.add_middleware(SessionMiddleware, secret_key=settings.secret_key)
# 挂载静态文件
app.mount("/static", StaticFiles(directory=settings.static_dir), name="static")
app.mount("/media", StaticFiles(directory=settings.media_dir), name="media")
# 注册API路由
app.include_router(api_router, prefix="/api")

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB