update img

This commit is contained in:
bridge
2025-10-30 00:47:50 +08:00
parent cf210d13e4
commit 5b2e1489e2
41 changed files with 546 additions and 2 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 127 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 1002 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -41,7 +41,7 @@ def load_avatar_images(pygame_mod, tile_size: int):
if filename.endswith('.png') and filename != 'original.png' and filename.replace('.png', '').isdigit():
image_path = os.path.join(base_dir, filename)
image = pygame_mod.image.load(image_path)
avatar_size = max(26, int((tile_size * 4 // 3) * 1.2))
avatar_size = max(26, int((tile_size * 4 // 3) * 1.5))
scaled = pygame_mod.transform.scale(image, (avatar_size, avatar_size))
results.append(scaled)
return results

View File

@@ -14,7 +14,7 @@ ai:
max_parse_retries: 3
game:
init_npc_num: 3
init_npc_num: 6
sect_num: 2 # init_npc_num大于sect_num时会随机选择sect_num个宗门
npc_birth_rate_per_month: 0.001
fortune_probability: 0.001

162
tools/img_gen/gen_img.py Normal file
View File

@@ -0,0 +1,162 @@
import os
import base64
from datetime import datetime
import requests
# 全局配置:请在此处填入你的 DashScope API Key
API_KEY = "sk-26818d3a4eb14b41a71f4d0319e4edfa" # <-- 在此处粘贴你的 API Key
BASE_URL = "https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation"
MODEL = "qwen-image-plus"
def generate_qwen_image(prompt: str, *, size: str = "1328*1328") -> str:
"""调用 DashScope 原生接口生成图片,返回 base64 字符串。
入参:
prompt: 生成图片的提示词
size: 图片尺寸,形如 "宽*高"(例如 "1328*1328"
返回:
base64 字符串(不带 data: 前缀),可自行解码保存为图片
"""
if not API_KEY:
raise RuntimeError("请先在代码顶部设置 API_KEY")
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {API_KEY}",
}
payload = {
"model": MODEL,
"input": {
"messages": [
{
"role": "user",
"content": [
{"text": prompt}
],
}
]
},
"parameters": {
"negative_prompt": "",
"prompt_extend": True,
"watermark": True,
"size": size,
},
}
r = requests.post(BASE_URL, headers=headers, json=payload, timeout=120)
r.raise_for_status()
data = r.json()
def extract_image_from_content(content_list):
"""从 content 列表中提取图片,优先 URL 后 base64"""
for item in content_list:
if not isinstance(item, dict):
continue
# 尝试提取 URL
url = item.get("image") or item.get("image_url") or item.get("url")
if isinstance(url, str) and url.startswith("http"):
img_bytes = requests.get(url, timeout=120).content
return base64.b64encode(img_bytes).decode("utf-8")
# 尝试提取 base64
b64 = item.get("b64") or item.get("b64_json") or item.get("image_base64")
if isinstance(b64, str) and len(b64) > 100:
return b64
return None
output = data.get("output", {})
# 尝试路径1output.choices[0].message.content[*]
choices = output.get("choices", [])
if choices:
content_list = choices[0].get("message", {}).get("content", [])
result = extract_image_from_content(content_list)
if result:
return result
# 尝试路径2output.results[0].content[*]
results = output.get("results", [])
if results:
content_list = results[0].get("content", [])
result = extract_image_from_content(content_list)
if result:
return result
raise RuntimeError("未获得图片结果")
def save_generated_image(query: str) -> str:
"""根据查询生成图片并保存到 result 目录。
入参:
query: 图片生成的提示词
返回:
保存的图片文件路径
"""
b64 = generate_qwen_image(query)
img_bytes = base64.b64decode(b64)
result_dir = "tools/img_gen/tmp/raw"
os.makedirs(result_dir, exist_ok=True)
filename = datetime.now().strftime("%Y%m%d_%H%M%S") + ".png"
out_path = os.path.join(result_dir, filename)
with open(out_path, "wb") as f:
f.write(img_bytes)
print(f"图片已保存: {out_path}")
return out_path
if __name__ == "__main__":
female_prompt_base = "一个好看的仙侠女性头像。只有头部和面部。二次元风格的漫画图片略微Q版正面看镜头。纯白背景。"
female_affixes = [
"紫色长发,表情嗔怒,带有一丝冷峻,有一个簪子。",
"乌黑直发,眉心一点红砂,清冷淡漠,镶玉步摇。",
"银白短发,英气微笑,发梢轻卷,耳坠为小灵铃。",
"墨绿长发,高马尾,目光坚毅,额前碎发,佩青竹簪。",
"渐变粉蓝长卷发,眸有星点,温柔含笑,薄纱额饰。",
"赤红披发,英气冷艳,眉尾上挑,凤羽发冠。",
"浅金长发,缎带系发,气质圣洁,流苏步摇。",
"乌青长发,微皱眉,眼尾红妆,一枚冰晶发卡。",
"白发如雪,神情淡然,眉心月印,玉质头箍。",
"靛蓝长发,俏皮眨眼,脸颊淡粉,葫芦小发簪。",
"茶棕双丸子头,活泼微笑,脸上淡淡雀斑,小葵花发卡。",
"青丝长发半披半挽,清雅端庄,蝶形玉簪。",
"淡紫短波浪发,俏皮吐舌,星月耳饰。",
"墨发低侧马尾,冷静专注,细链额饰垂坠。",
"湖绿挑染长发,狡黠微笑,狐耳发饰点缀。",
"灰蓝长直发,平刘海,面无表情,银环头饰。",
]
male_prompt_base = "一个英俊的的仙侠男性头像。只有头部和面部。二次元风格的漫画图片略微Q版正面看镜头。纯白背景。"
male_affixes = [
"乌发高束,剑眉星目,气质冷峻,青玉发冠。",
"银白长发,淡笑从容,额间玄纹,流苏头箍。",
"墨发披肩,脸上一抹浅疤,坚毅沉稳,黑金发簪。",
"深棕短发,目光凌厉,薄唇紧抿,皮绳束发。",
"蓝黑长发,发尾微卷,温润如玉,白玉簪。",
"赤褐长发,桀骜挑眉,轻笑不羁,耳坠小铜铃。",
"玄青半束发,沉静内敛,额前碎发,银纹额饰。",
"白发如雪,清隽淡笑,眉心一点冰蓝印,细环头饰。",
"墨发高马尾,目如寒星,英气逼人,羽纹发冠。",
"亚麻色短发,随性浅笑,轻胡茬,细革头环。",
"乌青长发,神情冷淡,眼神专注,剑形耳坠。",
"银灰长直发,肃杀气质,额缠黑带,简洁利落。",
"深紫挑染长发,狡黠微笑,眸底流光,狐尾发饰。",
"墨发半披,眼神温和从容,玉串发夹。",
"金棕长发,爽朗大笑,额前碎发,兽牙发簪。",
"青黑短发,专注坚定,线条硬朗,细链发饰垂坠。",
]
for affix in male_affixes:
prompt_text = male_prompt_base + affix
save_generated_image(prompt_text)
for affix in female_affixes:
prompt_text = female_prompt_base + affix
save_generated_image(prompt_text)

241
tools/img_gen/remove_bg.py Normal file
View File

@@ -0,0 +1,241 @@
from __future__ import annotations
from collections import deque
from pathlib import Path
from typing import Optional, Union
import numpy as np
from PIL import Image, ImageChops, ImageFilter
from tqdm import tqdm
PathLike = Union[str, Path]
def remove_white_background(
source: PathLike | Image.Image,
white_threshold: int = 240,
output: Optional[PathLike] = None,
show_progress: bool = False,
) -> Image.Image:
"""
移除图像中与边缘相连的白色背景,保留前景对象内部的浅色区域。
使用洪水填充算法,从图像四边开始,只标记与边缘相连的接近白色的像素为背景。
这样可以避免错误擦除前景对象内部的浅色区域。
Args:
source: 输入图像路径或 PIL Image 对象
white_threshold: 判定白色的阈值RGB三通道都大于此值才视为白色 (0-255)
output: 可选的输出路径
show_progress: 是否显示洪水填充的进度条
Returns:
处理后的 RGBA 图像,背景为透明
"""
if isinstance(source, (str, Path)):
with Image.open(source) as loaded:
image = loaded.convert("RGB")
else:
image = source.convert("RGB")
width, height = image.size
if width == 0 or height == 0:
result = Image.new("RGBA", (width, height), (0, 0, 0, 0))
if output is not None:
result.save(output)
return result
# 转换为numpy数组进行处理
img_array = np.array(image)
# 创建背景掩码False表示前景True表示背景
background_mask = np.zeros((height, width), dtype=bool)
# 创建访问标记
visited = np.zeros((height, width), dtype=bool)
# 洪水填充队列
queue = deque()
# 判断像素是否接近白色
def is_white(y, x):
pixel = img_array[y, x]
return pixel[0] >= white_threshold and pixel[1] >= white_threshold and pixel[2] >= white_threshold
# 将图像四边的白色像素加入队列
# 上边和下边
for x in range(width):
if is_white(0, x):
queue.append((0, x))
visited[0, x] = True
background_mask[0, x] = True
if is_white(height - 1, x):
queue.append((height - 1, x))
visited[height - 1, x] = True
background_mask[height - 1, x] = True
# 左边和右边(排除角落已处理的点)
for y in range(1, height - 1):
if is_white(y, 0):
queue.append((y, 0))
visited[y, 0] = True
background_mask[y, 0] = True
if is_white(y, width - 1):
queue.append((y, width - 1))
visited[y, width - 1] = True
background_mask[y, width - 1] = True
# 洪水填充:扩展所有与边缘相连的白色区域
directions = [(0, 1), (1, 0), (0, -1), (-1, 0)]
# 使用tqdm包装队列处理过程
if show_progress:
pbar = tqdm(total=len(queue), desc="Flood filling", unit="px")
while queue:
y, x = queue.popleft()
# 检查四个方向的相邻像素
for dy, dx in directions:
ny, nx = y + dy, x + dx
# 边界检查
if 0 <= ny < height and 0 <= nx < width and not visited[ny, nx]:
if is_white(ny, nx):
visited[ny, nx] = True
background_mask[ny, nx] = True
queue.append((ny, nx))
if show_progress:
pbar.total += 1
if show_progress:
pbar.update(1)
if show_progress:
pbar.close()
# 创建RGBA图像
result_array = np.zeros((height, width, 4), dtype=np.uint8)
result_array[:, :, :3] = img_array # 复制RGB通道
result_array[:, :, 3] = np.where(background_mask, 0, 255) # 设置Alpha通道
result = Image.fromarray(result_array, mode="RGBA")
if output is not None:
result.save(output)
return result
def crop_inner_region(
source: PathLike | Image.Image,
fraction: float = 1 / 16,
output: Optional[PathLike] = None,
) -> Image.Image:
"""裁剪图像四边各 ``fraction`` 宽度/高度,默认各去除 1/16。"""
if isinstance(source, (str, Path)):
with Image.open(source) as loaded:
image = loaded.convert("RGBA")
else:
image = source.copy()
width, height = image.size
if width == 0 or height == 0:
return image
fraction = max(0.0, min(fraction, 0.5))
dx = int(round(width * fraction))
dy = int(round(height * fraction))
left = min(dx, width // 2)
upper = min(dy, height // 2)
right = max(width - dx, left)
lower = max(height - dy, upper)
cropped = image.crop((left, upper, right, lower))
if output is not None:
cropped.save(output)
return cropped
def process(
source: PathLike | Image.Image,
*,
crop_fraction: float = 1 / 16,
white_threshold: int = 240,
output: Optional[PathLike] = None,
show_progress: bool = False,
) -> Image.Image:
"""先裁边后去白底的组合处理函数。"""
cropped = crop_inner_region(source, fraction=crop_fraction)
cleaned = remove_white_background(
cropped,
white_threshold=white_threshold,
show_progress=show_progress
)
if output is not None:
cleaned.save(output)
return cleaned
def process_all(
input_dir: PathLike = "result",
output_dir: PathLike = "processed",
*,
crop_fraction: float = 1 / 16,
white_threshold: int = 240,
show_progress: bool = True,
show_detail_progress: bool = False,
) -> list[Path]:
"""
遍历目录内的图像文件,批量处理并保存到目标目录。
Args:
input_dir: 输入目录
output_dir: 输出目录
crop_fraction: 裁剪比例
white_threshold: 白色阈值
show_progress: 是否显示批处理进度条
show_detail_progress: 是否显示每张图片的详细处理进度(洪水填充进度)
"""
input_path = Path(input_dir)
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
allowed_suffixes = {".png", ".jpg", ".jpeg", ".webp", ".bmp"}
files = [
path
for path in sorted(input_path.iterdir())
if path.is_file() and path.suffix.lower() in allowed_suffixes
]
iterator = tqdm(files, desc="Processing images") if show_progress else files
saved_files: list[Path] = []
for file_path in iterator:
target = output_path / file_path.name
process(
file_path,
crop_fraction=crop_fraction,
white_threshold=white_threshold,
output=target,
show_progress=show_detail_progress,
)
saved_files.append(target)
return saved_files
if __name__ == "__main__":
process_all(
input_dir="tools/img_gen/tmp/raw",
output_dir="tools/img_gen/tmp/processed",
crop_fraction=1 / 16,
)

View File

@@ -0,0 +1,21 @@
"""
PyInstaller hook for tiktoken
确保 tiktoken 的编码数据和插件被正确打包
"""
from PyInstaller.utils.hooks import collect_data_files, collect_submodules
# 收集 tiktoken 的所有数据文件
datas = collect_data_files('tiktoken')
# 收集所有子模块,包括 tiktoken_ext
hiddenimports = collect_submodules('tiktoken')
hiddenimports += collect_submodules('tiktoken_ext')
# 确保核心编码被导入
hiddenimports += [
'tiktoken.registry',
'tiktoken.core',
'tiktoken_ext.openai_public',
'tiktoken_ext',
]

102
tools/package/pack.ps1 Normal file
View File

@@ -0,0 +1,102 @@
$ErrorActionPreference = "Stop"
# Locate repository root directory
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$RepoRoot = (Resolve-Path (Join-Path $ScriptDir "..\..")).Path
# Get Git TAG
$tag = ""
Push-Location $RepoRoot
# Get exact tag
$exact = & git describe --tags --abbrev=0 --exact-match
if ($LASTEXITCODE -eq 0 -and $exact) {
$tag = $exact.Trim()
}
# Fallback: any readable description
if (-not $tag) {
$desc = & git describe --tags --dirty --always
if ($LASTEXITCODE -eq 0 -and $desc) {
$tag = $desc.Trim()
}
}
Pop-Location
if (-not $tag) {
Write-Error "Cannot get git tag. Please run in a Git repository."
exit 1
}
# Paths and directories
$DistDir = Join-Path $RepoRoot ("tmp\" + $tag)
$BuildDir = Join-Path $RepoRoot ("tmp\build\" + $tag)
$SpecDir = Join-Path $RepoRoot ("tmp\spec\" + $tag)
New-Item -ItemType Directory -Force -Path $DistDir, $BuildDir, $SpecDir | Out-Null
# Entry and app name
$EntryPy = Join-Path $RepoRoot "src\run\run.py"
$AppName = "CultivationWorld"
if (-not (Test-Path $EntryPy)) {
Write-Error "Entry script not found: $EntryPy"
exit 1
}
# Assets and static paths
$AssetsPath = Join-Path $RepoRoot "assets"
$StaticPath = Join-Path $RepoRoot "static"
# Runtime hook
$RuntimeHookPath = Join-Path $ScriptDir "runtime_hook_setcwd.py"
# Additional hooks directory
$AdditionalHooksPath = $ScriptDir
# Source path
$SrcPath = Join-Path $RepoRoot "src"
# Assemble PyInstaller arguments
$argsList = @(
$EntryPy,
"--name", $AppName,
"--onedir",
"--clean",
"--noconfirm",
"--windowed",
"--distpath", $DistDir,
"--workpath", $BuildDir,
"--specpath", $SpecDir,
"--paths", $RepoRoot,
"--additional-hooks-dir", $AdditionalHooksPath,
"--add-data", "${AssetsPath};assets",
"--add-data", "${StaticPath};static",
"--hidden-import", "tiktoken_ext.openai_public",
"--hidden-import", "tiktoken_ext",
"--collect-all", "tiktoken",
"--collect-all", "litellm",
"--copy-metadata", "tiktoken",
"--copy-metadata", "litellm"
)
if (Test-Path $RuntimeHookPath) {
$argsList += @("--runtime-hook", $RuntimeHookPath)
}
# Call PyInstaller
Push-Location $RepoRoot
try {
& pyinstaller @argsList
} finally {
Pop-Location
}
# Copy cmd files
$CmdSrc = Join-Path $ScriptDir "set_env.cmd"
if (Test-Path $CmdSrc) {
Copy-Item -Path $CmdSrc -Destination $DistDir -Force
}
Write-Host "Package completed: " (Resolve-Path $DistDir).Path
Write-Host "Executable directory: " (Join-Path $DistDir $AppName)

View File

@@ -0,0 +1,18 @@
import os
import sys
def _set_cwd_to_exe_dir() -> None:
# 在 PyInstaller 运行时,将工作目录切换到可执行文件所在目录
try:
if hasattr(sys, "_MEIPASS"):
base_dir = os.path.dirname(sys.executable)
else:
base_dir = os.path.dirname(os.path.abspath(sys.argv[0]))
os.chdir(base_dir)
except Exception:
# 保持默认工作目录
pass
_set_cwd_to_exe_dir()