diff --git a/apps/stories/management/commands/generate_default_covers.py b/apps/stories/management/commands/generate_default_covers.py new file mode 100644 index 0000000..ad7c64d --- /dev/null +++ b/apps/stories/management/commands/generate_default_covers.py @@ -0,0 +1,116 @@ +""" +用新的 LLM 提炼逻辑重新生成默认故事封面并上传到 OSS。 + +使用方法: + python manage.py generate_default_covers + python manage.py generate_default_covers --dry-run # 仅打印提炼到的描述,不生成图片 +""" +import uuid +import logging + +import requests +from django.conf import settings +from django.core.management.base import BaseCommand + +from apps.stories.utils import DEFAULT_STORIES +from apps.stories.services.llm_service import _extract_image_description + +logger = logging.getLogger(__name__) + +# 每个默认故事对应的 OSS key(与 utils.py 中的 cover_url 一致) +DEFAULT_COVER_KEYS = { + "失控的魔法扫帚": "stories/defaults/失控的魔法扫帚_cover.png", +} + + +class Command(BaseCommand): + help = "用 LLM 提炼故事画面描述后调用 Seedream 4.5 重新生成默认故事封面" + + def add_arguments(self, parser): + parser.add_argument( + "--dry-run", + action="store_true", + help="仅打印 LLM 提炼的画面描述,不实际生成图片", + ) + + def handle(self, *args, **options): + dry_run = options["dry_run"] + config = settings.LLM_CONFIG + + if not config.get("API_KEY"): + self.stderr.write(self.style.ERROR("VOLCENGINE_API_KEY 未配置")) + return + + try: + from volcenginesdkarkruntime import Ark + except ImportError: + self.stderr.write(self.style.ERROR("volcengine SDK 未安装")) + return + + try: + from utils.oss import get_oss_client + import oss2 + except ImportError: + self.stderr.write(self.style.ERROR("oss2 未安装")) + return + + client = Ark(api_key=config["API_KEY"]) + image_model = config.get("IMAGE_MODEL_NAME", "doubao-seedream-4-5-251128") + image_size = config.get("IMAGE_SIZE", "2560x1440") + oss_config = settings.ALIYUN_OSS + oss_base = f"https://{oss_config['BUCKET_NAME']}.{oss_config['ENDPOINT']}" + + for story in DEFAULT_STORIES: + title = story["title"] + content = story["content"] + oss_key = DEFAULT_COVER_KEYS.get(title) + + if not oss_key: + self.stdout.write(self.style.WARNING(f"[{title}] 未找到对应 OSS key,跳过")) + continue + + self.stdout.write(f"\n[{title}]") + + # Step 1: LLM 提炼画面描述 + self.stdout.write(" 正在用 LLM 提炼画面描述...") + scene_desc = _extract_image_description( + title, content, client, config["MODEL_NAME"] + ) + self.stdout.write(self.style.SUCCESS(f" 画面描述({len(scene_desc)} 字):{scene_desc}")) + + if dry_run: + self.stdout.write(self.style.NOTICE(" [dry-run] 跳过图片生成")) + continue + + # Step 2: 文生图 + image_prompt = ( + f"儿童绘本封面插画,{scene_desc},卡通可爱风格,色彩明亮鲜艳,高质量插画" + ) + self.stdout.write(f" 正在生成封面图({image_size})...") + result = client.images.generate( + model=image_model, + prompt=image_prompt, + size=image_size, + response_format="url", + watermark=False, + ) + image_url = result.data[0].url + self.stdout.write(f" 临时图片 URL: {image_url[:80]}...") + + # Step 3: 下载图片 + self.stdout.write(" 正在下载图片...") + resp = requests.get(image_url, timeout=60) + resp.raise_for_status() + + # Step 4: 覆盖上传到 OSS + self.stdout.write(f" 正在上传到 OSS: {oss_key}") + oss_client = get_oss_client() + oss_client.bucket.put_object( + oss_key, + resp.content, + headers={"Content-Type": "image/jpeg"}, + ) + final_url = f"{oss_base}/{oss_key}" + self.stdout.write(self.style.SUCCESS(f" ✓ 封面已更新: {final_url}")) + + self.stdout.write(self.style.SUCCESS("\n完成。"))