feat: implement template service with builtin and custom templates
- BUILTIN_TEMPLATES: 4 presets (正方形/纵向/横向长方形/左胸小号) - load_custom_templates(): reads templates.json, skips corrupt entries individually so one bad record never blocks the rest - get_all_templates(): builtin first, then custom - add_template(): upsert by name, forces type="custom" - rename_template() / delete_template(): raise ValueError if not found - _save_custom_templates(): auto-creates config/ dir, logs on failure Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,2 +1,191 @@
|
||||
def load_templates():
|
||||
return []
|
||||
import json
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
from core.models import Template
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_TEMPLATES_FILENAME = "templates.json"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 内置模板(代码定义,不可删除/修改)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
BUILTIN_TEMPLATES: List[Template] = [
|
||||
Template(
|
||||
name="正方形模板",
|
||||
x_ratio=0.25, y_ratio=0.25,
|
||||
width_ratio=0.50, height_ratio=0.50,
|
||||
rotation=0.0, type="builtin",
|
||||
),
|
||||
Template(
|
||||
name="纵向长方形模板",
|
||||
x_ratio=0.30, y_ratio=0.15,
|
||||
width_ratio=0.40, height_ratio=0.55,
|
||||
rotation=0.0, type="builtin",
|
||||
),
|
||||
Template(
|
||||
name="横向长方形模板",
|
||||
x_ratio=0.15, y_ratio=0.30,
|
||||
width_ratio=0.70, height_ratio=0.40,
|
||||
rotation=0.0, type="builtin",
|
||||
),
|
||||
Template(
|
||||
name="左胸小号模板",
|
||||
x_ratio=0.35, y_ratio=0.28,
|
||||
width_ratio=0.12, height_ratio=0.12,
|
||||
rotation=0.0, type="builtin",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 内部辅助
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _templates_file():
|
||||
from services.file_service import get_config_path
|
||||
return get_config_path(_TEMPLATES_FILENAME)
|
||||
|
||||
|
||||
def _validate_entry(data) -> Optional[Template]:
|
||||
"""Parse one raw dict into a Template; return None and log on any error."""
|
||||
try:
|
||||
name = str(data["name"]).strip()
|
||||
if not name:
|
||||
raise ValueError("name is empty")
|
||||
w = float(data["width_ratio"])
|
||||
h = float(data["height_ratio"])
|
||||
if w <= 0 or h <= 0:
|
||||
raise ValueError("width_ratio and height_ratio must be > 0")
|
||||
return Template(
|
||||
name=name,
|
||||
x_ratio=float(data.get("x_ratio", 0.0)),
|
||||
y_ratio=float(data.get("y_ratio", 0.0)),
|
||||
width_ratio=w,
|
||||
height_ratio=h,
|
||||
rotation=float(data.get("rotation", 0.0)),
|
||||
type="custom",
|
||||
)
|
||||
except (KeyError, ValueError, TypeError) as exc:
|
||||
logger.warning("Skipping invalid template entry (%s): %s", exc, data)
|
||||
return None
|
||||
|
||||
|
||||
def _save_custom_templates(templates: List[Template]):
|
||||
"""Write custom templates to JSON. Logs error on failure, does not raise."""
|
||||
path = _templates_file()
|
||||
try:
|
||||
path.parent.mkdir(exist_ok=True)
|
||||
payload = {
|
||||
"templates": [
|
||||
{
|
||||
"name": t.name,
|
||||
"x_ratio": t.x_ratio,
|
||||
"y_ratio": t.y_ratio,
|
||||
"width_ratio": t.width_ratio,
|
||||
"height_ratio": t.height_ratio,
|
||||
"rotation": t.rotation,
|
||||
}
|
||||
for t in templates
|
||||
]
|
||||
}
|
||||
with open(str(path), "w", encoding="utf-8") as f:
|
||||
json.dump(payload, f, ensure_ascii=False, indent=2)
|
||||
logger.info("Saved %d custom template(s) to %s", len(templates), path)
|
||||
except OSError as exc:
|
||||
logger.error("Failed to save templates: %s", exc)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 公开 API
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def get_builtin_templates() -> List[Template]:
|
||||
"""Return a copy of the built-in template list."""
|
||||
return list(BUILTIN_TEMPLATES)
|
||||
|
||||
|
||||
def load_custom_templates() -> List[Template]:
|
||||
"""Load user-defined templates from JSON.
|
||||
|
||||
Returns [] on missing file. Skips corrupt individual entries so a
|
||||
single bad record never blocks the rest.
|
||||
"""
|
||||
path = _templates_file()
|
||||
if not path.exists():
|
||||
return []
|
||||
|
||||
try:
|
||||
with open(str(path), encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
except (json.JSONDecodeError, OSError) as exc:
|
||||
logger.warning("Cannot read templates.json (%s), ignoring.", exc)
|
||||
return []
|
||||
|
||||
if not isinstance(data, dict) or "templates" not in data:
|
||||
logger.warning("templates.json has unexpected structure, ignoring.")
|
||||
return []
|
||||
|
||||
results = []
|
||||
for entry in data["templates"]:
|
||||
t = _validate_entry(entry)
|
||||
if t is not None:
|
||||
results.append(t)
|
||||
|
||||
logger.info("Loaded %d custom template(s) from %s", len(results), path)
|
||||
return results
|
||||
|
||||
|
||||
def get_all_templates() -> List[Template]:
|
||||
"""Return builtin templates followed by custom templates."""
|
||||
return get_builtin_templates() + load_custom_templates()
|
||||
|
||||
|
||||
def add_template(template: Template):
|
||||
"""Add or overwrite a custom template, then persist.
|
||||
|
||||
If a custom template with the same name already exists it is replaced.
|
||||
Built-in templates cannot be added via this function.
|
||||
"""
|
||||
template.type = "custom"
|
||||
customs = load_custom_templates()
|
||||
customs = [t for t in customs if t.name != template.name]
|
||||
customs.append(template)
|
||||
_save_custom_templates(customs)
|
||||
logger.info("Template added: %s", template.name)
|
||||
|
||||
|
||||
def rename_template(old_name: str, new_name: str):
|
||||
"""Rename a custom template and persist.
|
||||
|
||||
Raises ValueError if *old_name* is not found or *new_name* is empty.
|
||||
"""
|
||||
new_name = new_name.strip()
|
||||
if not new_name:
|
||||
raise ValueError("New template name cannot be empty")
|
||||
customs = load_custom_templates()
|
||||
found = False
|
||||
for t in customs:
|
||||
if t.name == old_name:
|
||||
t.name = new_name
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
raise ValueError("Custom template not found: {}".format(old_name))
|
||||
_save_custom_templates(customs)
|
||||
logger.info("Template renamed: %s -> %s", old_name, new_name)
|
||||
|
||||
|
||||
def delete_template(name: str):
|
||||
"""Delete a custom template by name and persist.
|
||||
|
||||
Raises ValueError if *name* is not found among custom templates.
|
||||
"""
|
||||
customs = load_custom_templates()
|
||||
filtered = [t for t in customs if t.name != name]
|
||||
if len(filtered) == len(customs):
|
||||
raise ValueError("Custom template not found: {}".format(name))
|
||||
_save_custom_templates(filtered)
|
||||
logger.info("Template deleted: %s", name)
|
||||
|
||||
@@ -185,19 +185,19 @@
|
||||
|
||||
任务:
|
||||
|
||||
- [ ] 先读取 `src/services/template_service.py` 现有内容
|
||||
- [ ] 完善 `src/services/template_service.py`
|
||||
- [ ] 定义内置模板
|
||||
- [ ] 支持读取自定义模板 JSON
|
||||
- [ ] 支持保存自定义模板 JSON
|
||||
- [ ] 支持新增、重命名、删除自定义模板
|
||||
- [ ] 校验模板字段
|
||||
- [x] 先读取 `src/services/template_service.py` 现有内容
|
||||
- [x] 完善 `src/services/template_service.py`
|
||||
- [x] 定义内置模板
|
||||
- [x] 支持读取自定义模板 JSON
|
||||
- [x] 支持保存自定义模板 JSON
|
||||
- [x] 支持新增、重命名、删除自定义模板
|
||||
- [x] 校验模板字段
|
||||
|
||||
验收:
|
||||
|
||||
- [ ] 内置模板和自定义模板可区分
|
||||
- [ ] 单个模板损坏不影响全部模板加载
|
||||
- [ ] UI 不直接操作模板 JSON 文件
|
||||
- [x] 内置模板和自定义模板可区分
|
||||
- [x] 单个模板损坏不影响全部模板加载
|
||||
- [x] UI 不直接操作模板 JSON 文件
|
||||
|
||||
## 5. 图片合成核心
|
||||
|
||||
|
||||
Reference in New Issue
Block a user