diff --git a/apps/stories/services/llm_service.py b/apps/stories/services/llm_service.py index f9b5fdc..c165914 100644 --- a/apps/stories/services/llm_service.py +++ b/apps/stories/services/llm_service.py @@ -122,12 +122,28 @@ def generate_story_stream(characters, scenes, props): result = _parse_story_json(full_content) + # ── Generate cover image ── + yield _sse_event('stage', { + 'stage': 'cover', + 'progress': 90, + 'message': '正在绘制故事封面...', + }) + + cover_url = '' + try: + cover_url = _generate_and_upload_cover( + result['title'], characters, scenes, props, config + ) + except Exception as cover_err: + logger.warning(f'Cover generation failed (non-fatal): {cover_err}') + yield _sse_event('done', { 'stage': 'done', 'progress': 100, 'message': '大功告成!', 'title': result['title'], 'content': result['content'], + 'cover_url': cover_url, }) except Exception as e: @@ -157,6 +173,64 @@ def _parse_story_json(text): } +def _generate_and_upload_cover(title, characters, scenes, props, config): + """ + 使用豆包文生图模型生成故事封面,上传到 OSS 并返回 URL。 + 失败时抛出异常(由调用方捕获,不影响主流程)。 + """ + import uuid + import requests as req_lib + from datetime import datetime + from django.conf import settings + from volcenginesdkarkruntime import Ark + + # Build a concise Chinese image prompt + elements = [] + if characters: + elements.append('、'.join(characters)) + if scenes: + elements.append('、'.join(scenes)) + if props: + elements.append('、'.join(props)) + + element_str = ','.join(elements) if elements else title + image_prompt = ( + f"儿童绘本封面插画,{title},{element_str}," + "卡通可爱风格,色彩明亮鲜艳,温馨有趣,适合3-8岁儿童,高质量插画" + ) + + 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', '1920x1920') + + result = client.images.generate( + model=image_model, + prompt=image_prompt, + size=image_size, + response_format='url', + watermark=False, + ) + + image_url = result.data[0].url + + # Download from temporary URL and upload to OSS + resp = req_lib.get(image_url, timeout=60) + resp.raise_for_status() + + from utils.oss import get_oss_client + oss_client = get_oss_client() + key = f"stories/covers/{datetime.now().strftime('%Y%m%d')}/{uuid.uuid4().hex}.jpg" + oss_client.bucket.put_object( + key, resp.content, + headers={'Content-Type': 'image/jpeg'}, + ) + + oss_config = settings.ALIYUN_OSS + if oss_config.get('CUSTOM_DOMAIN'): + return f"https://{oss_config['CUSTOM_DOMAIN']}/{key}" + return f"https://{oss_config['BUCKET_NAME']}.{oss_config['ENDPOINT']}/{key}" + + def _sse_event(event, data): """格式化 SSE 事件""" return f"event: {event}\ndata: {json.dumps(data, ensure_ascii=False)}\n\n" diff --git a/config/settings.py b/config/settings.py index e2920fe..ca6e1f9 100644 --- a/config/settings.py +++ b/config/settings.py @@ -198,6 +198,8 @@ ALIYUN_PHONE_AUTH = { LLM_CONFIG = { 'API_KEY': os.environ.get('VOLCENGINE_API_KEY', ''), 'MODEL_NAME': os.environ.get('VOLCENGINE_MODEL_NAME', 'doubao-seed-1-6-lite-251015'), + 'IMAGE_MODEL_NAME': os.environ.get('VOLCENGINE_IMAGE_MODEL_NAME', 'doubao-seedream-4-5-251128'), + 'IMAGE_SIZE': os.environ.get('VOLCENGINE_IMAGE_SIZE', '1920x1920'), } # Swagger/OpenAPI Settings