Files
wordcloud/backend/core/config.py
T

352 lines
12 KiB
Python

import argparse
import json
import logging
import os
import random
import sys
from pathlib import Path
import numpy as np
from PIL import ImageFont
from . import paths
BASE_DIR = paths.BASE_DIR
RUNTIME_DIR = paths.RUNTIME_DIR
ASSETS_DIR = paths.ASSETS_DIR
FONTS_DIR = paths.FONTS_DIR
PROJECT_DEFAULT_FONT = paths.PROJECT_DEFAULT_FONT
# 配置日志:只写入文件,不干扰控制台输出
logging.basicConfig(
filename=str(RUNTIME_DIR / "ewc_concurrency.log"),
level=logging.DEBUG,
format='%(asctime)s - %(levelname)s - %(message)s',
filemode='w'
)
# ==================== 0. 配置区(默认值) ====================
MODE = "IMAGE"
# --- Image Mode ---
MASK_IMAGE_PATH = "7887.png"
IMAGE_CANVAS_MODE = "WIDTH"
FILL_CORNERS = False
CORNER_FILL_RATIO = 0.15
# --- Text Mode ---
MASK_TEXT = "A"
MASK_FONT_PATH = str(PROJECT_DEFAULT_FONT)
MASK_FONT_SIZE = 3000
# --- 自动画幅与清晰度 ---
# 默认 4k 级画布:打印/激光足够清晰,比 8k 渲染快约 4×
BASE_HD_WIDTH = 4000
BASE_HD_HEIGHT = 4000
MIN_READABLE_HEIGHT_PX = 22
# 运算网格缩放:0.18 在速度/质量之间更均衡
WORK_SCALE = 0.18
# 服务端快速预览路径:减少试探次数,但保留真实字形碰撞和零重叠校验。
FAST_MODE = False
# --- 阴阳刻 ---
FILL_ON = "BLACK"
# --- 数据与字体 ---
EXCEL_PATH = "四个方向汇总录取名单.xlsx"
DATA_COL_INDEX = 1
WEIGHT_COL_INDEX = None
WEIGHT_COL_NAME = None
REMOVE_DUPLICATES = False
ENABLE_STROKE_WEIGHTS = True
WC_FONT_PATH = str(PROJECT_DEFAULT_FONT)
FONT_FALLBACK_PATHS = (
"/System/Library/Fonts/STHeiti Medium.ttc",
"/System/Library/Fonts/Hiragino Sans GB.ttc",
"/Library/Fonts/Arial Unicode.ttf",
)
# --- 填充策略 ---
N_REPETITIONS = 1
# 名单较少、掩膜轮廓填不满时,自动循环追加名字副本增加词数,让费马螺旋
# 能走到掩膜远端(心形尖端、人物四肢),把形状填出来而非退化成圆形。
# AUTO_REPEAT_MAX 是自动重复次数的安全上限,避免无限追加。
AUTO_REPEAT_TO_FILL = True
AUTO_REPEAT_MAX = 20
# 面积模型目标填充率:中文实心笔画像素占比约 0.35–0.55。
# 略偏保守以保证 scale=1.0 首次就能放满,减少多轮重试。
TARGET_FILL_RATIO = 0.45
SIZE_RATIO = 2.0
PACKING_EFFICIENCY = 0.9
# 竖排概率:每个词独立以此概率竖着摆放,其余水平摆放。
# 0.0 = 全部水平,1.0 = 全部竖排。适度混排可打散过于规整的观感。
VERTICAL_RATIO = 0.18
# --- 智能字号搜索 ---
MIN_FONT_SIZE = int(MIN_READABLE_HEIGHT_PX * WORK_SCALE)
USER_MIN_FONT_SIZE = None
USER_MAX_FONT_SIZE = None
MIN_FONT_FLOOR = 2
# --- 画布重试 ---
CANVAS_RETRY_MAX_ROUNDS = 1
CANVAS_RETRY_GROWTH = 1.12
# --- 配色 ---
DARK_COLOR_PALETTE = (
"#102A43",
"#1F4E5F",
"#206A5D",
"#7B341E",
"#5D1F45",
)
LIGHT_COLOR_PALETTE = (
"#EAF2FF",
"#CDECF6",
"#CFF7E6",
"#FFD8C2",
"#F6D1EB",
)
FONT_COLOR = "#000000" # 统一字体颜色,None 则使用调色板
# --- 输出 ---
OUTPUT_DIR = "."
OUTPUT_PREFIX = ""
OUTPUT_PNG = "Efficient_Result_HD_AutoResize.png"
OUTPUT_SVG = "Efficient_Result_HD_AutoResize.svg"
DB_PATH = "wordcloud_hd.db"
METRICS_FILE = "metrics.json"
SAVE_DEBUG_IMAGES = False
DEBUG_OUTPUT_DIR = "output"
# --- 可复现性 ---
SEED = None
LAYOUT_SEED = None
KNOWN_CONFIG_KEYS = {
'MODE', 'MASK_IMAGE_PATH', 'IMAGE_CANVAS_MODE', 'FILL_CORNERS',
'CORNER_FILL_RATIO', 'MASK_TEXT', 'MASK_FONT_PATH', 'MASK_FONT_SIZE',
'BASE_HD_WIDTH', 'BASE_HD_HEIGHT', 'MIN_READABLE_HEIGHT_PX', 'WORK_SCALE', 'FAST_MODE', 'FILL_ON', 'EXCEL_PATH',
'DATA_COL_INDEX', 'WEIGHT_COL_INDEX', 'WEIGHT_COL_NAME', 'REMOVE_DUPLICATES', 'ENABLE_STROKE_WEIGHTS',
'WC_FONT_PATH',
'FONT_FALLBACK_PATHS',
'N_REPETITIONS', 'TARGET_FILL_RATIO', 'SIZE_RATIO', 'PACKING_EFFICIENCY', 'VERTICAL_RATIO',
'AUTO_REPEAT_TO_FILL', 'AUTO_REPEAT_MAX',
'USER_MIN_FONT_SIZE', 'USER_MAX_FONT_SIZE',
'CANVAS_RETRY_MAX_ROUNDS', 'CANVAS_RETRY_GROWTH',
'DARK_COLOR_PALETTE', 'LIGHT_COLOR_PALETTE', 'FONT_COLOR', 'OUTPUT_DIR', 'OUTPUT_PREFIX',
'OUTPUT_PNG', 'OUTPUT_SVG', 'DB_PATH', 'METRICS_FILE', 'SAVE_DEBUG_IMAGES', 'DEBUG_OUTPUT_DIR', 'SEED',
'LAYOUT_SEED'
}
CONFIG_ALIASES = {
'seed': 'SEED',
'layout_seed': 'LAYOUT_SEED',
'excel_path': 'EXCEL_PATH',
'mask_image_path': 'MASK_IMAGE_PATH',
'output_dir': 'OUTPUT_DIR',
'output_prefix': 'OUTPUT_PREFIX',
'mode': 'MODE',
'work_scale': 'WORK_SCALE',
'weight_col_index': 'WEIGHT_COL_INDEX',
'weight_col_name': 'WEIGHT_COL_NAME',
'min_font_size': 'USER_MIN_FONT_SIZE',
'max_font_size': 'USER_MAX_FONT_SIZE',
'font_color': 'FONT_COLOR',
'stroke_weights': 'ENABLE_STROKE_WEIGHTS',
'auto_repeat_to_fill': 'AUTO_REPEAT_TO_FILL',
'auto_repeat_max': 'AUTO_REPEAT_MAX',
'save_debug_images': 'SAVE_DEBUG_IMAGES',
}
CRITICAL_TYPE_CHECKS = {
'MODE': str,
'WORK_SCALE': (int, float),
'FAST_MODE': bool,
'DATA_COL_INDEX': int,
'WEIGHT_COL_INDEX': (int, type(None)),
'WEIGHT_COL_NAME': (str, type(None)),
'FONT_FALLBACK_PATHS': (list, tuple),
'USER_MIN_FONT_SIZE': (int, float, type(None)),
'USER_MAX_FONT_SIZE': (int, float, type(None)),
'SAVE_DEBUG_IMAGES': bool,
'REMOVE_DUPLICATES': bool,
'ENABLE_STROKE_WEIGHTS': bool,
'CANVAS_RETRY_MAX_ROUNDS': int,
'CANVAS_RETRY_GROWTH': (int, float),
'AUTO_REPEAT_TO_FILL': bool,
'AUTO_REPEAT_MAX': int,
'VERTICAL_RATIO': (int, float),
'SEED': (int, type(None)),
'LAYOUT_SEED': (int, type(None)),
}
DEFAULT_CONFIG = {k: v for k, v in globals().items() if k in KNOWN_CONFIG_KEYS}
def parse_args():
parser = argparse.ArgumentParser(description="Efficient WordCloud generator")
parser.add_argument("--config", type=str, help="JSON 配置文件路径")
parser.add_argument("--seed", type=int, help="随机种子(可复现)")
parser.add_argument("--layout-seed", type=int, help="布局顺序随机种子")
parser.add_argument("--excel-path", type=str, help="Excel 输入路径")
parser.add_argument("--mask-image-path", type=str, help="掩膜图片路径(IMAGE 模式)")
parser.add_argument("--output-dir", type=str, help="输出目录")
parser.add_argument("--output-prefix", type=str, help="输出文件前缀")
parser.add_argument("--mode", type=str, choices=["TEXT", "IMAGE"], help="掩膜模式")
parser.add_argument("--work-scale", type=float, help="运算缩放比例")
parser.add_argument("--weight-col-index", type=int, help="Excel 权重列索引")
parser.add_argument("--weight-col-name", type=str, help="Excel 权重列名(优先于索引)")
parser.add_argument("--min-font-size", type=float, help="覆盖最小字号")
parser.add_argument("--max-font-size", type=float, help="覆盖最大字号")
return parser.parse_args()
def _warn(msg):
print(f"[WARN] {msg}")
def _resolve_path(path_str):
p = Path(path_str)
if p.is_absolute():
return p
return BASE_DIR / p
def _with_prefix(filename, prefix):
if not prefix:
return filename
return f"{prefix}_{filename}"
def apply_json_config(config_path):
cfg_path = _resolve_path(config_path)
if not cfg_path.exists():
print(f"错误: 配置文件不存在: {cfg_path}")
sys.exit(1)
try:
with cfg_path.open("r", encoding="utf-8") as f:
data = json.load(f)
except Exception as e:
print(f"错误: 读取配置文件失败: {e}")
sys.exit(1)
if not isinstance(data, dict):
print("错误: 配置文件顶层必须是 JSON 对象")
sys.exit(1)
normalized_data = {}
for key, value in data.items():
key_upper = CONFIG_ALIASES.get(key, key)
normalized_data[key_upper] = value
for key in normalized_data.keys():
if key not in KNOWN_CONFIG_KEYS:
_warn(f"未知配置键: {key}")
for key, expected in CRITICAL_TYPE_CHECKS.items():
if key in normalized_data and not isinstance(normalized_data[key], expected):
print(f"错误: 配置键 {key} 类型错误,期望 {expected},实际 {type(normalized_data[key])}")
sys.exit(1)
for key, value in normalized_data.items():
if key in KNOWN_CONFIG_KEYS:
globals()[key] = value
def apply_cli_overrides(args):
mapping = {
'seed': 'SEED',
'layout_seed': 'LAYOUT_SEED',
'excel_path': 'EXCEL_PATH',
'mask_image_path': 'MASK_IMAGE_PATH',
'output_dir': 'OUTPUT_DIR',
'output_prefix': 'OUTPUT_PREFIX',
'mode': 'MODE',
'work_scale': 'WORK_SCALE',
'weight_col_index': 'WEIGHT_COL_INDEX',
'weight_col_name': 'WEIGHT_COL_NAME',
'min_font_size': 'USER_MIN_FONT_SIZE',
'max_font_size': 'USER_MAX_FONT_SIZE',
}
for arg_key, cfg_key in mapping.items():
value = getattr(args, arg_key)
if value is not None:
globals()[cfg_key] = value
def _resolve_font_path(configured_path, fallback_paths, *, role):
candidates = [_resolve_path(configured_path), *[Path(path) for path in fallback_paths]]
errors = []
for index, candidate in enumerate(candidates):
if not candidate.exists():
errors.append(f"{candidate}: missing")
continue
try:
ImageFont.truetype(str(candidate), 32)
if index == 0:
print(f"[Font] {role} 使用项目字体: {candidate}")
else:
_warn(f"{role} 字体未命中项目内资源,回退到系统字体: {candidate}")
return str(candidate)
except OSError as exc:
errors.append(f"{candidate}: {exc}")
print(f"错误: {role} 字体初始化失败。候选路径: {errors}")
sys.exit(1)
def finalize_runtime_config():
global EXCEL_PATH, MASK_IMAGE_PATH, MASK_FONT_PATH, WC_FONT_PATH
global OUTPUT_DIR, OUTPUT_PNG, OUTPUT_SVG, DB_PATH, METRICS_FILE, DEBUG_OUTPUT_DIR, MIN_FONT_SIZE
global LAYOUT_SEED
# 运行时派生字段
MIN_FONT_SIZE = int(MIN_READABLE_HEIGHT_PX * WORK_SCALE)
if LAYOUT_SEED is None:
LAYOUT_SEED = SEED
EXCEL_PATH = str(_resolve_path(EXCEL_PATH))
MASK_IMAGE_PATH = str(_resolve_path(MASK_IMAGE_PATH))
MASK_FONT_PATH = _resolve_font_path(MASK_FONT_PATH, FONT_FALLBACK_PATHS, role="mask")
WC_FONT_PATH = _resolve_font_path(WC_FONT_PATH, FONT_FALLBACK_PATHS, role="layout")
OUTPUT_DIR = str(_resolve_path(OUTPUT_DIR))
Path(OUTPUT_DIR).mkdir(parents=True, exist_ok=True)
OUTPUT_PNG = str(Path(OUTPUT_DIR) / _with_prefix(Path(OUTPUT_PNG).name, OUTPUT_PREFIX))
OUTPUT_SVG = str(Path(OUTPUT_DIR) / _with_prefix(Path(OUTPUT_SVG).name, OUTPUT_PREFIX))
DB_PATH = str(Path(OUTPUT_DIR) / _with_prefix(Path(DB_PATH).name, OUTPUT_PREFIX))
METRICS_FILE = str(Path(OUTPUT_DIR) / _with_prefix(Path(METRICS_FILE).name, OUTPUT_PREFIX))
DEBUG_OUTPUT_DIR = str(Path(OUTPUT_DIR) / Path(DEBUG_OUTPUT_DIR).name)
def set_random_seed():
if SEED is None:
return
np.random.seed(SEED)
random.seed(SEED)
print(f"[Seed] 使用固定随机种子: {SEED}")
def get_output_background():
return "black" if FILL_ON == "WHITE" else "white"
def get_output_palette():
return LIGHT_COLOR_PALETTE if FILL_ON == "WHITE" else DARK_COLOR_PALETTE
def write_metrics(metrics):
try:
with Path(METRICS_FILE).open("w", encoding="utf-8") as f:
json.dump(metrics, f, ensure_ascii=False, indent=2)
print(f"已保存: {METRICS_FILE}")
except Exception as e:
_warn(f"写入 metrics 失败(不影响主产物): {e}")