| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647 |
- import json
- import os
- import re
- import pandas as pd
- import requests
- from natsort.natsort import natsorted
- from PIL import Image, ImageDraw, ImageFont
- import settings
- from logger import logger
- PLATE_LONG = 2048
- PLATE_SIZE = (1366, 2048)
- PLATE_COLOR = (186, 186, 186)
- OUTPUT_NAME = "场景套图"
- MARKER_NAME = "场景套图_任务.json"
- TEMP_NAME = "_场景套图临时"
- LAYOUT_PLATE = {
- "handheld": {"height_ratio": 0.30, "bottom_ratio": 0.10, "max_width_ratio": 0.66},
- "floor": {"height_ratio": 0.44, "bottom_ratio": 0.02, "max_width_ratio": 0.90},
- }
- ANCHOR_LABELS = {
- "top-left": "左上",
- "top-right": "右上",
- "bottom-left": "左下",
- "bottom-right": "右下",
- "center": "中间",
- }
- SKIP_FOLDERS = {"原始图", "原始图_已抠图", "800x800", "阴影图处理", "200images", OUTPUT_NAME, TEMP_NAME}
- def prepare_scene_set(payload):
- goods_dir = resolve_goods_dir(payload)
- cutout_dir = os.path.join(goods_dir, "原始图_已抠图")
- if not os.path.isdir(cutout_dir):
- raise RuntimeError("没有找到原始图_已抠图,不会重新抠图")
- files = natsorted([
- name for name in os.listdir(cutout_dir)
- if name.lower().endswith(".png") and os.path.isfile(os.path.join(cutout_dir, name))
- ])
- if not files:
- raise RuntimeError("原始图_已抠图里没有 PNG,不会重新抠图")
- layout = payload.get("layout") or "handheld"
- slots = [slot for slot in (payload.get("slots") or []) if (slot.get("layout") or layout) == layout]
- slots = sorted(slots, key=lambda slot: int(slot.get("seq") or 0))
- cutouts = []
- for index, name in enumerate(files):
- slot = slots[index] if index < len(slots) else {}
- cutouts.append({
- "file": name,
- "path": os.path.join(cutout_dir, name),
- "index": index + 1,
- "slot_id": slot.get("id") or 0,
- "name": slot.get("name") or "未识别",
- "role": slot.get("role") or "",
- "rotation": int(slot.get("rotation") or 0),
- "use_long_scene": int(slot.get("use_long_scene") or 0),
- "use_three_view": int(slot.get("use_three_view") or 0),
- "use_detail": int(slot.get("use_detail") or 0),
- "use_for_scene": int(slot.get("use_for_scene") or 0) if slot else 0,
- "crops": slot.get("crops") or [],
- })
- output_dir = os.path.join(goods_dir, OUTPUT_NAME)
- logger.info("场景套图使用已有抠图 goods=%s dir=%s count=%s", payload.get("goods_art_no"), goods_dir, len(cutouts))
- return {
- "goods_dir": goods_dir,
- "cutout_dir": cutout_dir,
- "output_dir": output_dir,
- "output_exists": os.path.isdir(output_dir),
- "cutouts": cutouts,
- "message": "使用已有抠图,没有重新抠图",
- }
- def run_scene_set(payload):
- goods_dir = resolve_goods_dir(payload)
- output_dir = os.path.join(goods_dir, OUTPUT_NAME)
- if os.path.isdir(output_dir):
- return {
- "skipped": True,
- "reason": "已有场景套图目录,要重出请先删除该目录",
- "output_dir": output_dir,
- "goods_dir": goods_dir,
- }
- marker = os.path.join(goods_dir, MARKER_NAME)
- if os.path.isfile(marker) and not int(payload.get("force") or 0):
- saved = json.loads(open(marker, "r", encoding="utf-8").read())
- if saved.get("job_id"):
- return {
- "resumed": True,
- "job_id": saved["job_id"],
- "goods_dir": goods_dir,
- "output_dir": output_dir,
- "reason": "继续上次未完成的任务",
- }
- cutouts = payload.get("cutouts") or []
- source = next((item for item in cutouts if int(item.get("use_for_scene") or 0) and int(item.get("use_long_scene") or 0)), None)
- if not source:
- raise RuntimeError("没有可用于场景的机位")
- layout = payload.get("layout") or "handheld"
- temp_dir = os.path.join(goods_dir, TEMP_NAME)
- os.makedirs(temp_dir, exist_ok=True)
- plate_path = os.path.join(temp_dir, "plate.jpg")
- png_path = os.path.join(temp_dir, "cutout.png")
- build_plate(source["path"], int(source.get("rotation") or 0), layout, png_path, plate_path)
- logger.info("场景套图上传抠图 PNG,不转成 JPEG %s", png_path)
- upload_file(png_path)
- plate_url = upload_file(plate_path)
- materials = collect_materials(payload)
- body = {
- "category_id": int(payload.get("category_id") or 0),
- "goods_art_no": payload.get("goods_art_no") or "",
- "basis": payload.get("basis") or "selling_point",
- "scene_tags": payload.get("scene_tags") or [],
- "materials": materials,
- "cutouts": [public_cutout(item) for item in cutouts],
- "copy_anchor": payload.get("copy_anchor") or "top-left",
- "plate_url": plate_url,
- "options": {
- "layout": layout,
- "height_cm": payload.get("height_cm") or "",
- "missing_height": payload.get("missing_height") or "skip",
- "default_tier": payload.get("default_tier") or "M",
- "visible_features": payload.get("visible_features") or [],
- },
- }
- data = backend_post("/product_scene/create_job", body)
- if data.get("job_id"):
- with open(marker, "w", encoding="utf-8") as file:
- json.dump({"job_id": data["job_id"]}, file, ensure_ascii=False)
- data["goods_dir"] = goods_dir
- data["output_dir"] = output_dir
- data["message"] = "使用已有抠图,没有重新抠图"
- return data
- def save_scene_set(payload):
- goods_dir = payload.get("goods_dir") or ""
- if not goods_dir or not os.path.isdir(goods_dir):
- raise RuntimeError("货号目录不存在")
- job = payload.get("job") or {}
- cutouts = payload.get("cutouts") or []
- anchor = job.get("copy_anchor") or payload.get("copy_anchor") or "top-left"
- output_dir = os.path.join(goods_dir, OUTPUT_NAME)
- os.makedirs(output_dir, exist_ok=True)
- saved = []
- notes = list(job.get("progress_notes") or [])
- for image in job.get("images") or []:
- kind = image.get("kind")
- if kind == "scene":
- if int(image.get("status") or 0) != 2 or not image.get("result_url"):
- notes.append((image.get("scene_name") or "场景") + " 未出图:" + (image.get("error_msg") or "失败"))
- continue
- stem = "场景_{:02d}_{}".format(int(image.get("sort") or 1), safe_name(image.get("scene_name") or "场景"))
- jpg_path = os.path.join(output_dir, stem + ".jpg")
- download(image["result_url"], jpg_path)
- png_path = os.path.join(output_dir, stem + ".png")
- compose_copy(Image.open(jpg_path).convert("RGB"), anchor, image.get("title") or "", image.get("lines") or []).save(png_path, "PNG")
- saved.append(jpg_path)
- saved.append(png_path)
- elif kind == "three_view":
- path = os.path.join(output_dir, "三视图.png")
- render_three_view(cutouts, image, anchor).save(path, "PNG")
- saved.append(path)
- elif kind == "detail":
- path = os.path.join(output_dir, "细节.png")
- render_detail(cutouts, image, anchor).save(path, "PNG")
- saved.append(path)
- elif kind in ("size", "param"):
- filename = "尺寸.png" if kind == "size" else "参数.png"
- path = os.path.join(output_dir, filename)
- render_text_page(image.get("title") or filename[:-4], image.get("lines") or [], anchor).save(path, "PNG")
- saved.append(path)
- txt_path = os.path.join(output_dir, "卖点.txt")
- with open(txt_path, "w", encoding="utf-8") as file:
- file.write(selling_text(job, anchor, notes))
- saved.append(txt_path)
- marker = os.path.join(goods_dir, MARKER_NAME)
- if os.path.isfile(marker):
- os.remove(marker)
- logger.info("场景套图已写入 %s", output_dir)
- return {"output_dir": output_dir, "files": saved, "notes": notes}
- def resolve_goods_dir(payload):
- goods_dir = payload.get("goods_dir") or ""
- if goods_dir and os.path.isdir(goods_dir) and has_cutout_png(goods_dir):
- return os.path.abspath(goods_dir)
- image_dir = goods_dir_from_image(payload.get("image_path") or "")
- if image_dir and has_cutout_png(image_dir):
- return image_dir
- found = find_output_goods_dir(payload.get("goods_art_no") or "")
- if found:
- return found
- if image_dir:
- return image_dir
- if goods_dir and os.path.isdir(goods_dir):
- return os.path.abspath(goods_dir)
- raise RuntimeError("没有找到货号目录")
- def goods_dir_from_image(image_path):
- if not image_path or not os.path.exists(image_path):
- return ""
- parent = os.path.dirname(os.path.abspath(image_path))
- if os.path.basename(parent) in SKIP_FOLDERS:
- parent = os.path.dirname(parent)
- return parent
- def has_cutout_png(goods_dir):
- cutout_dir = os.path.join(goods_dir, "原始图_已抠图")
- if not os.path.isdir(cutout_dir):
- return False
- for name in os.listdir(cutout_dir):
- path = os.path.join(cutout_dir, name)
- if name.lower().endswith(".png") and os.path.isfile(path):
- return True
- return False
- def find_output_goods_dir(goods_art_no):
- goods_art_no = str(goods_art_no or "").strip()
- output_dir = str(settings.OUTPUT_DIR)
- if not goods_art_no or not os.path.isdir(output_dir):
- return ""
- matches = []
- try:
- names = os.listdir(output_dir)
- except OSError:
- return ""
- for date_dir in names:
- date_path = os.path.join(output_dir, date_dir)
- goods_path = os.path.join(date_path, goods_art_no)
- if os.path.isdir(date_path) and has_cutout_png(goods_path):
- matches.append(os.path.abspath(goods_path))
- if not matches:
- return ""
- matches.sort(key=lambda path: os.path.basename(os.path.dirname(path)))
- return matches[-1]
- def build_plate(source_path, rotation, layout, png_path, plate_path):
- image = Image.open(source_path).convert("RGBA")
- if rotation:
- image = image.rotate(-int(rotation), expand=True)
- image = trim_alpha(image)
- image = fit_long_edge(image, PLATE_LONG)
- image.save(png_path, "PNG")
- ratios = LAYOUT_PLATE.get(layout) or LAYOUT_PLATE["handheld"]
- canvas = Image.new("RGB", PLATE_SIZE, PLATE_COLOR)
- width, height = image.size
- scale = min(
- ratios["height_ratio"] * canvas.height / height,
- ratios["max_width_ratio"] * canvas.width / width,
- )
- resized = image.resize((max(1, int(width * scale)), max(1, int(height * scale))), Image.Resampling.LANCZOS)
- x = int((canvas.width - resized.width) / 2)
- y = int(canvas.height - ratios["bottom_ratio"] * canvas.height - resized.height)
- canvas.paste(resized, (x, y), resized.getchannel("A"))
- canvas.save(plate_path, "JPEG", quality=90)
- def trim_alpha(image, threshold=16, pad=12):
- alpha = image.getchannel("A")
- mask = alpha.point(lambda value: 255 if value > threshold else 0)
- box = mask.getbbox()
- if not box:
- return image
- left, top, right, bottom = box
- left = max(0, left - pad)
- top = max(0, top - pad)
- right = min(image.width, right + pad)
- bottom = min(image.height, bottom + pad)
- return image.crop((left, top, right, bottom))
- def fit_long_edge(image, long_edge):
- width, height = image.size
- longest = max(width, height)
- if longest <= long_edge:
- return image
- scale = long_edge / float(longest)
- return image.resize((max(1, int(width * scale)), max(1, int(height * scale))), Image.Resampling.LANCZOS)
- def upload_file(path):
- ext = os.path.splitext(path)[1].lower()
- content_type = "image/png" if ext == ".png" else "image/jpeg"
- with open(path, "rb") as file:
- result = requests.post(
- settings.DOMAIN + "/api/upload",
- files={"file": (os.path.basename(path), file, content_type)},
- timeout=120,
- ).json()
- url = ((result or {}).get("data") or {}).get("url")
- if not url:
- raise RuntimeError("图片上传失败")
- return url
- def collect_materials(payload):
- fields = dict(payload.get("materials") or {})
- selling = (payload.get("selling_points") or "").strip()
- excel_path = payload.get("excel_path") or ""
- if excel_path:
- parsed = read_excel(excel_path, payload.get("goods_art_no") or "")
- for key, value in parsed["fields"].items():
- fields.setdefault(key, value)
- if not selling:
- selling = parsed["selling_points"]
- return {"fields": fields, "selling_points": selling}
- def read_excel(path, goods_art_no):
- if not os.path.isfile(path):
- raise RuntimeError("资料文件不存在")
- frame = pd.read_excel(path)
- frame.columns = [str(column).strip() for column in frame.columns]
- if {"字段", "内容"}.issubset(set(frame.columns)):
- fields = {}
- selling = []
- for _, row in frame.iterrows():
- name = "" if pd.isna(row["字段"]) else str(row["字段"]).strip()
- value = "" if pd.isna(row["内容"]) else str(row["内容"]).strip()
- if not name or not value:
- continue
- if name in ("卖点", "卖点文案"):
- selling.append(value)
- else:
- fields[name] = value
- return {"fields": fields, "selling_points": "\n".join(selling)}
- row = frame.iloc[0]
- if "货号" in frame.columns and goods_art_no:
- matched = frame[frame["货号"].astype(str).str.strip() == str(goods_art_no).strip()]
- if matched.empty:
- raise RuntimeError("资料里没有这个货号")
- row = matched.iloc[0]
- fields = {}
- selling = []
- for column, value in row.items():
- if pd.isna(value):
- continue
- text = str(value).strip()
- if not text or column == "货号":
- continue
- if column in ("卖点", "卖点文案"):
- selling.append(text)
- else:
- fields[column] = text
- return {"fields": fields, "selling_points": "\n".join(selling)}
- def public_cutout(item):
- return {
- "name": item.get("name") or "",
- "role": item.get("role") or "",
- "use_three_view": int(item.get("use_three_view") or 0),
- "use_detail": int(item.get("use_detail") or 0),
- "use_for_scene": int(item.get("use_for_scene") or 0),
- "use_long_scene": int(item.get("use_long_scene") or 0),
- "crops": item.get("crops") or [],
- }
- def backend_post(path, payload):
- response = requests.post(
- settings.DOMAIN + "/api/ai_image/camera_machine" + path,
- headers={
- "Authorization": "Bearer {}".format(settings.USER_TOKEN or ""),
- "Content-Type": "application/json",
- },
- json=payload,
- timeout=180,
- )
- try:
- body = response.json()
- except Exception:
- raise RuntimeError("后端没有返回结果")
- if body.get("code") not in (0, "0"):
- raise RuntimeError(body.get("message") or body.get("msg") or "后端请求失败")
- return body.get("data") or {}
- def download(url, path):
- response = requests.get(url, timeout=180)
- response.raise_for_status()
- with open(path, "wb") as file:
- file.write(response.content)
- def compose_copy(base, anchor, title, lines):
- canvas = base.convert("RGBA")
- scale = canvas.width / 750.0
- title_lines = split_title(title)
- body = [str(line).strip() for line in lines if str(line).strip() and not str(line).startswith("卖点")]
- if not title_lines and not body:
- return canvas.convert("RGB")
- title_font = load_font(int(34 * scale), bold=True)
- body_font = load_font(int(20 * scale), bold=False)
- draw = ImageDraw.Draw(canvas)
- max_width = int(canvas.width * 0.72)
- wrapped = []
- for line in title_lines:
- wrapped.extend(wrap_text(line, title_font, max_width, draw))
- body_wrapped = []
- for line in body:
- body_wrapped.extend(wrap_text(line, body_font, max_width, draw))
- block = measure_block(draw, wrapped, title_font, body_wrapped, body_font, scale)
- if anchor == "center":
- origin = (int((canvas.width - block[0]) / 2), int((canvas.height - block[1]) / 2))
- plate = Image.new("RGBA", canvas.size, (0, 0, 0, 0))
- plate_draw = ImageDraw.Draw(plate)
- pad = int(28 * scale)
- plate_draw.rounded_rectangle(
- [origin[0] - pad, origin[1] - pad, origin[0] + block[0] + pad, origin[1] + block[1] + pad],
- radius=int(18 * scale),
- fill=(0, 0, 0, 140),
- )
- canvas = Image.alpha_composite(canvas, plate)
- else:
- canvas = Image.alpha_composite(canvas, shade_layer(canvas.size, anchor))
- origin = text_origin(canvas.size, block, anchor, scale)
- draw = ImageDraw.Draw(canvas)
- y = origin[1]
- for line in wrapped:
- x = align_x(origin[0], block[0], draw.textlength(line, font=title_font), anchor)
- draw.text((x, y), line, font=title_font, fill=(255, 255, 255, 255))
- y += int(46 * scale)
- if wrapped and body_wrapped:
- y += int(8 * scale)
- for line in body_wrapped:
- x = align_x(origin[0], block[0], draw.textlength(line, font=body_font), anchor)
- draw.text((x, y), line, font=body_font, fill=(255, 255, 255, 230))
- y += int(32 * scale)
- return canvas.convert("RGB")
- def render_three_view(cutouts, image, anchor):
- canvas = Image.new("RGB", (1500, 2400), (243, 240, 235))
- plan = ((image.get("extra") or {}).get("plan") or [])
- margin, gap = 48, 28
- card_w = 1500 - margin * 2
- card_h = (2400 - margin * 2 - gap * 2) // 3
- y = margin
- for item in plan:
- card = product_card(find_cutout(cutouts, item.get("role")), card_w, card_h, item.get("label") or "")
- canvas.paste(card, (margin, y))
- y += card_h + gap
- return compose_copy(canvas, anchor, image.get("title") or "", image.get("lines") or [])
- def render_detail(cutouts, image, anchor):
- canvas = Image.new("RGB", (1500, 2400), (243, 240, 235))
- plan = ((image.get("extra") or {}).get("plan") or [])[:4]
- if not plan:
- return compose_copy(canvas, anchor, image.get("title") or "", image.get("lines") or [])
- margin, gap = 48, 24
- card_w = 1500 - margin * 2
- card_h = (2400 - margin * 2 - gap * (len(plan) - 1)) // len(plan)
- y = margin
- for item in plan:
- cutout = find_cutout(cutouts, item.get("role"), item.get("name"))
- cropped = crop_box(open_cutout(cutout), item.get("box") or [0, 0, 1, 1]) if cutout else None
- card = product_card_image(cropped, card_w, card_h, item.get("label") or "细节")
- canvas.paste(card, (margin, y))
- y += card_h + gap
- return compose_copy(canvas, anchor, image.get("title") or "", image.get("lines") or [])
- def render_text_page(title, lines, anchor):
- canvas = Image.new("RGB", (1500, 2400), (243, 240, 235))
- return compose_copy(canvas, anchor, title, lines)
- def product_card(cutout, width, height, label):
- image = open_cutout(cutout) if cutout else None
- return product_card_image(image, width, height, label)
- def product_card_image(image, width, height, label):
- card = Image.new("RGB", (width, height), (255, 255, 255))
- draw = ImageDraw.Draw(card)
- font = load_font(28, bold=False)
- label_h = 64
- if image:
- fitted = fit_box(image, width - 48, height - label_h - 48)
- x = int((width - fitted.width) / 2)
- y = int((height - label_h - fitted.height) / 2)
- card.paste(fitted, (x, max(16, y)), fitted.getchannel("A"))
- text = label or ""
- draw.text(((width - draw.textlength(text, font=font)) / 2, height - label_h), text, font=font, fill=(40, 40, 40, 255))
- return card
- def open_cutout(cutout):
- if not cutout or not cutout.get("path") or not os.path.isfile(cutout["path"]):
- return None
- image = Image.open(cutout["path"]).convert("RGBA")
- rotation = int(cutout.get("rotation") or 0)
- if rotation:
- image = image.rotate(-rotation, expand=True)
- return trim_alpha(image)
- def crop_box(image, box):
- if image is None:
- return None
- left, top, right, bottom = box
- width, height = image.size
- return image.crop((
- int(float(left) * width),
- int(float(top) * height),
- int(float(right) * width),
- int(float(bottom) * height),
- ))
- def find_cutout(cutouts, role, name=""):
- for item in cutouts:
- if role and item.get("role") == role:
- return item
- for item in cutouts:
- if name and item.get("name") == name:
- return item
- return None
- def fit_box(image, width, height):
- scale = min(width / image.width, height / image.height)
- return image.resize((max(1, int(image.width * scale)), max(1, int(image.height * scale))), Image.Resampling.LANCZOS)
- def shade_layer(size, anchor):
- width, height = size
- mask = Image.new("L", size, 0)
- draw = ImageDraw.Draw(mask)
- span = int(height * 0.46)
- if anchor in ("top-left", "top-right"):
- for y in range(span):
- draw.line([(0, y), (width, y)], fill=int(170 * (1 - y / float(span))))
- elif anchor in ("bottom-left", "bottom-right"):
- start = height - span
- for y in range(start, height):
- draw.line([(0, y), (width, y)], fill=int(170 * ((y - start) / float(span))))
- layer = Image.new("RGBA", size, (0, 0, 0, 0))
- layer.putalpha(mask)
- return layer
- def split_title(title):
- text = str(title or "").replace("卖点:", "").replace("卖点:", "").strip()
- if not text:
- return []
- return [part.strip() for part in re.split(r"\s*/\s*", text) if part.strip()]
- def wrap_text(text, font, max_width, draw):
- lines = []
- current = ""
- for char in str(text):
- trial = current + char
- if draw.textlength(trial, font=font) <= max_width:
- current = trial
- else:
- if current:
- lines.append(current)
- current = char
- if current:
- lines.append(current)
- return lines
- def measure_block(draw, title_lines, title_font, body_lines, body_font, scale):
- width = 0
- for line in title_lines:
- width = max(width, draw.textlength(line, font=title_font))
- for line in body_lines:
- width = max(width, draw.textlength(line, font=body_font))
- height = len(title_lines) * int(46 * scale) + len(body_lines) * int(32 * scale)
- if title_lines and body_lines:
- height += int(8 * scale)
- return int(width), int(height)
- def text_origin(size, block, anchor, scale):
- pad = int(42 * scale)
- width, height = size
- block_w, block_h = block
- if anchor == "top-left":
- return pad, pad
- if anchor == "top-right":
- return width - pad - block_w, pad
- if anchor == "bottom-left":
- return pad, height - pad - block_h
- if anchor == "bottom-right":
- return width - pad - block_w, height - pad - block_h
- return int((width - block_w) / 2), int((height - block_h) / 2)
- def align_x(origin_x, block_w, line_w, anchor):
- if anchor in ("top-right", "bottom-right"):
- return origin_x + block_w - line_w
- if anchor == "center":
- return origin_x + (block_w - line_w) / 2
- return origin_x
- def load_font(size, bold=False):
- path = r"C:\Windows\Fonts\msyhbd.ttc" if bold else r"C:\Windows\Fonts\msyh.ttc"
- if not os.path.isfile(path):
- path = r"C:\Windows\Fonts\msyh.ttc"
- if not os.path.isfile(path):
- raise RuntimeError("没有找到微软雅黑")
- return ImageFont.truetype(path, max(12, int(size)), index=0)
- def selling_text(job, anchor, notes):
- rows = ["来源:{}".format(job.get("selling_source") or ""), "文案位置:{}".format(ANCHOR_LABELS.get(anchor, anchor)), "", "条目"]
- points = job.get("selling_points") or []
- if not points:
- rows.append("(无)")
- for index, point in enumerate(points, start=1):
- tags = "、".join(point.get("tags") or [])
- rows.append("{}. {}".format(index, point.get("text") or ""))
- rows.append("依据:{}".format(point.get("evidence") or ""))
- if tags:
- rows.append("标签:{}".format(tags))
- rows.append("")
- rows.append("图片")
- for image in job.get("images") or []:
- status = {0: "等待", 1: "生成中", 2: "成功", 4: "本地", -1: "失败"}.get(int(image.get("status") or 0), str(image.get("status")))
- rows.append("{} {} {}".format(image.get("kind") or "", image.get("scene_name") or "", status))
- if image.get("lines"):
- rows.append("文案:" + " / ".join(image.get("lines") or []))
- if image.get("error_msg"):
- rows.append("说明:" + image.get("error_msg"))
- if notes:
- rows.append("")
- rows.append("进度")
- rows.extend(notes)
- return "\n".join(rows) + "\n"
- def safe_name(name):
- return re.sub(r'[\\/:*?"<>|]', "_", str(name))[:40]
|