diff --git a/assets/females/1.png b/assets/females/1.png index 5921d00..2fd7549 100644 Binary files a/assets/females/1.png and b/assets/females/1.png differ diff --git a/assets/females/10.png b/assets/females/10.png index 6a32ba8..efc7ff1 100644 Binary files a/assets/females/10.png and b/assets/females/10.png differ diff --git a/assets/females/11.png b/assets/females/11.png index efbebe5..fa6b40e 100644 Binary files a/assets/females/11.png and b/assets/females/11.png differ diff --git a/assets/females/12.png b/assets/females/12.png index 0341c23..e08cb6f 100644 Binary files a/assets/females/12.png and b/assets/females/12.png differ diff --git a/assets/females/13.png b/assets/females/13.png index f49010c..0cbdc5d 100644 Binary files a/assets/females/13.png and b/assets/females/13.png differ diff --git a/assets/females/14.png b/assets/females/14.png index 0bfdd95..67710b6 100644 Binary files a/assets/females/14.png and b/assets/females/14.png differ diff --git a/assets/females/15.png b/assets/females/15.png index d5042ae..7b9466b 100644 Binary files a/assets/females/15.png and b/assets/females/15.png differ diff --git a/assets/females/16.png b/assets/females/16.png index 7079103..aae3480 100644 Binary files a/assets/females/16.png and b/assets/females/16.png differ diff --git a/assets/females/2.png b/assets/females/2.png index 54c0571..5bb1220 100644 Binary files a/assets/females/2.png and b/assets/females/2.png differ diff --git a/assets/females/3.png b/assets/females/3.png index 7cd1b50..38593c8 100644 Binary files a/assets/females/3.png and b/assets/females/3.png differ diff --git a/assets/females/4.png b/assets/females/4.png index 796a23c..34c5543 100644 Binary files a/assets/females/4.png and b/assets/females/4.png differ diff --git a/assets/females/5.png b/assets/females/5.png index c6edacd..88bf1e3 100644 Binary files a/assets/females/5.png and b/assets/females/5.png differ diff --git a/assets/females/6.png b/assets/females/6.png index 76c8e0a..95525d4 100644 Binary files a/assets/females/6.png and b/assets/females/6.png differ diff --git a/assets/females/7.png b/assets/females/7.png index fbc1a1c..80f543d 100644 Binary files a/assets/females/7.png and b/assets/females/7.png differ diff --git a/assets/females/8.png b/assets/females/8.png index 17f5cfb..65c4c1e 100644 Binary files a/assets/females/8.png and b/assets/females/8.png differ diff --git a/assets/females/9.png b/assets/females/9.png index edef666..0d8c783 100644 Binary files a/assets/females/9.png and b/assets/females/9.png differ diff --git a/assets/females/original.png b/assets/females/original.png deleted file mode 100644 index 7f58107..0000000 Binary files a/assets/females/original.png and /dev/null differ diff --git a/assets/males/1.png b/assets/males/1.png index 54c8312..4d46e24 100644 Binary files a/assets/males/1.png and b/assets/males/1.png differ diff --git a/assets/males/10.png b/assets/males/10.png index 05c85f9..65ae243 100644 Binary files a/assets/males/10.png and b/assets/males/10.png differ diff --git a/assets/males/11.png b/assets/males/11.png index 7368c3a..cc7771b 100644 Binary files a/assets/males/11.png and b/assets/males/11.png differ diff --git a/assets/males/12.png b/assets/males/12.png index cb191a7..ce9d6bf 100644 Binary files a/assets/males/12.png and b/assets/males/12.png differ diff --git a/assets/males/13.png b/assets/males/13.png index 74b439c..b6f9a63 100644 Binary files a/assets/males/13.png and b/assets/males/13.png differ diff --git a/assets/males/14.png b/assets/males/14.png index 0436d52..bfb5ff0 100644 Binary files a/assets/males/14.png and b/assets/males/14.png differ diff --git a/assets/males/15.png b/assets/males/15.png index ac3fceb..2e0fa74 100644 Binary files a/assets/males/15.png and b/assets/males/15.png differ diff --git a/assets/males/16.png b/assets/males/16.png deleted file mode 100644 index cd603c4..0000000 Binary files a/assets/males/16.png and /dev/null differ diff --git a/assets/males/2.png b/assets/males/2.png index c0a09b4..73eb453 100644 Binary files a/assets/males/2.png and b/assets/males/2.png differ diff --git a/assets/males/3.png b/assets/males/3.png index 8d2d527..546bef6 100644 Binary files a/assets/males/3.png and b/assets/males/3.png differ diff --git a/assets/males/4.png b/assets/males/4.png index 26846de..fc36c21 100644 Binary files a/assets/males/4.png and b/assets/males/4.png differ diff --git a/assets/males/5.png b/assets/males/5.png index e7fb1db..9e0e7fd 100644 Binary files a/assets/males/5.png and b/assets/males/5.png differ diff --git a/assets/males/6.png b/assets/males/6.png index a23d156..a8c4208 100644 Binary files a/assets/males/6.png and b/assets/males/6.png differ diff --git a/assets/males/7.png b/assets/males/7.png index ab8fe6b..17ef0c8 100644 Binary files a/assets/males/7.png and b/assets/males/7.png differ diff --git a/assets/males/8.png b/assets/males/8.png index 8e0a29b..9a4e9d0 100644 Binary files a/assets/males/8.png and b/assets/males/8.png differ diff --git a/assets/males/9.png b/assets/males/9.png index a95aa84..c12caf2 100644 Binary files a/assets/males/9.png and b/assets/males/9.png differ diff --git a/assets/males/original.png b/assets/males/original.png deleted file mode 100644 index ab66ea1..0000000 Binary files a/assets/males/original.png and /dev/null differ diff --git a/src/front/assets.py b/src/front/assets.py index 53b8ad5..eda2d16 100644 --- a/src/front/assets.py +++ b/src/front/assets.py @@ -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 diff --git a/static/config.yml b/static/config.yml index 970afc3..48ad404 100644 --- a/static/config.yml +++ b/static/config.yml @@ -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 diff --git a/tools/img_gen/gen_img.py b/tools/img_gen/gen_img.py new file mode 100644 index 0000000..8d5d38d --- /dev/null +++ b/tools/img_gen/gen_img.py @@ -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", {}) + + # 尝试路径1:output.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 + + # 尝试路径2:output.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) \ No newline at end of file diff --git a/tools/img_gen/remove_bg.py b/tools/img_gen/remove_bg.py new file mode 100644 index 0000000..5a0939b --- /dev/null +++ b/tools/img_gen/remove_bg.py @@ -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, + ) \ No newline at end of file diff --git a/tools/package/hook-tiktoken.py b/tools/package/hook-tiktoken.py new file mode 100644 index 0000000..a33f2fc --- /dev/null +++ b/tools/package/hook-tiktoken.py @@ -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', +] + diff --git a/tools/package/pack.ps1 b/tools/package/pack.ps1 new file mode 100644 index 0000000..b7d611f --- /dev/null +++ b/tools/package/pack.ps1 @@ -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) \ No newline at end of file diff --git a/tools/package/runtime_hook_setcwd.py b/tools/package/runtime_hook_setcwd.py new file mode 100644 index 0000000..9066fe6 --- /dev/null +++ b/tools/package/runtime_hook_setcwd.py @@ -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() + +