update img
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 1.4 MiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 123 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 122 KiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 125 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 1002 KiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 110 KiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 121 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 1.3 MiB |
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 118 KiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 114 KiB After Width: | Height: | Size: 1.2 MiB |
|
Before Width: | Height: | Size: 1.2 MiB |
@@ -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
|
||||
|
||||
@@ -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
@@ -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)
|
||||
241
tools/img_gen/remove_bg.py
Normal 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,
|
||||
)
|
||||
21
tools/package/hook-tiktoken.py
Normal 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
@@ -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)
|
||||
18
tools/package/runtime_hook_setcwd.py
Normal 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()
|
||||
|
||||
|
||||