", f'
') + return f'
- 期望返回 JSON,形如
+ 期望返回 JSON(多标签,2-5 个),形如
+ 最终 prompt 会拼成英文描述发给文生图模型
diff --git a/backend/app/models/llm_setting.py b/backend/app/models/llm_setting.py index 3d6369e..3d81653 100644 --- a/backend/app/models/llm_setting.py +++ b/backend/app/models/llm_setting.py @@ -29,7 +29,7 @@ class LlmSetting(Base): image_prompt_template: Mapped[str | None] = mapped_column(Text) # === 插图参数 === - image_size: Mapped[str] = mapped_column(String(16), default="1024x768", nullable=False) + image_size: Mapped[str] = mapped_column(String(16), default="768x512", nullable=False) # === 模型 === chat_model: Mapped[str] = mapped_column(String(64), default="agnes-2.0-flash", nullable=False) diff --git a/backend/app/schemas/llm.py b/backend/app/schemas/llm.py index fabc4ea..15b53af 100644 --- a/backend/app/schemas/llm.py +++ b/backend/app/schemas/llm.py @@ -13,7 +13,7 @@ class LlmSettingOut(BaseModel): classify_prompt: str | None = None commentary_prompt: str | None = None image_prompt_template: str | None = None - image_size: str = "1024x768" + image_size: str = "768x512" chat_model: str = "agnes-2.0-flash" image_model: str = "agnes-image-2.1-flash" interval_sec: float = 2.0 @@ -48,10 +48,10 @@ DEFAULT_PROMPTS = { "原文:\n{body}\n" ), "classify_prompt": ( - "你是新闻分类助手。请阅读以下新闻,返回 1-2 个分类标签。\n" - "可选标签(可自由组合): 时政 / 经济 / 科技 / 军事 / 社会 / 国际 / 体育 / 文化 / 环境 / 健康 / 金融 / 能源 / 气候\n" - "严格要求:只返回 JSON,形如 {\"categories\": [\"时政\", \"国际\"]},不要其他内容。\n\n" - "标题:{title}\n摘要:{summary}\n" + "你是新闻分类助手。请阅读以下新闻,返回 2-5 个最相关的分类标签(多标签)。\n" + "可选标签(可自由组合,不限于此): 时政 / 经济 / 科技 / 军事 / 社会 / 国际 / 体育 / 文化 / 环境 / 健康 / 金融 / 能源 / 气候\n" + "严格要求:只返回 JSON,形如 {\"categories\": [\"时政\", \"国际\", \"经济\"]},不要其他内容。\n\n" + "标题:{title}\n摘要:{summary}\n正文(节选):{body}\n" ), "commentary_prompt": ( "你是资深新闻评论员。请基于以下新闻写一段 100-200 字的中文点评。\n" @@ -59,7 +59,7 @@ DEFAULT_PROMPTS = { "标题:{title}\n正文:{body}\n" ), "image_prompt_template": ( - "Editorial news illustration about: {title}. " + "Editorial news illustration inspired by: {body}. " "Cinematic, professional journalism style, soft natural lighting, " "no text, no logos, no watermark." ), diff --git a/backend/app/services/llm/enrichment.py b/backend/app/services/llm/enrichment.py index 0bf10a7..25ea80e 100644 --- a/backend/app/services/llm/enrichment.py +++ b/backend/app/services/llm/enrichment.py @@ -2,10 +2,17 @@ 4 个独立任务: 1. format — 排版译文(写入 body_zh_formatted) - 2. classify — 分类(写入 category) - 3. image — 生成插图(写入 image_ai_url) + 2. classify — 分类(写入 category,多标签) + 3. image — 生成插图(写入 image_ai_url,prompt 用正文第一段) 4. commentary — 写点评(写入 commentary) +排版容器 CSS(固定,不再让用户改): +- 字体: system-ui 字体栈 +- 字号: 17px +- 行高: 1.7 +- 颜色: #3e3e3e +- 段落: margin-bottom 1.5em(自动空一行) + 设计: - 任务入口: enrich_article(article_id, settings_row) - 任务间互不影响:每个任务独立 try/except + 写 status @@ -28,6 +35,25 @@ from app.services.llm.client import LlmClient logger = logging.getLogger("news.llm.enrichment") +# === 排版容器固定 CSS(项目级固定,不再让用户改)=== +# 同时内联到 body_zh_formatted 的容器 div 的 style 属性上, +# 保证分享/邮件/导出场景下样式不丢;前端全局 .article-body 类做兜底。 +ARTICLE_BODY_FONT_FAMILY = ( + "system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, " + "'Helvetica Neue', sans-serif" +) +ARTICLE_BODY_FONT_SIZE = "17px" +ARTICLE_BODY_LINE_HEIGHT = "1.7" +ARTICLE_BODY_COLOR = "#3e3e3e" +ARTICLE_BODY_P_MARGIN_BOTTOM = "1.5em" + +# === 插图默认尺寸(适中,不再用 1024x768)=== +# 写死到 enrichment 里,行为稳定;setting.image_size 仍可由用户在 UI 改, +# 但默认行为不依赖它,避免意外被改成很大。 +DEFAULT_IMAGE_SIZE = "768x512" +DEFAULT_IMAGE_FIRST_PARA_CHARS = 400 # 提取第一段最多用这么多字 +DEFAULT_IMAGE_MAX_TAGS = 5 # 分类标签上限(多标签) + # === 获取当前设置(行锁 + 缓存刷新)=== async def get_setting() -> LlmSetting: @@ -60,39 +86,81 @@ async def _enrich_format(article: Article, setting: LlmSetting, client: LlmClien temperature=0.3, max_tokens=2000, ) - # 极简 HTML 包裹:按段切 +
+ # 极简 HTML 包裹:按段切 +
,整体包到带固定 CSS 的
{p.strip()}
" for p in text.split("\n\n") if p.strip()] - article.body_zh_formatted = "\n".join(parts) or None + if not parts: + article.body_zh_formatted = None + else: + article.body_zh_formatted = _wrap_article_body("\n".join(parts)) article.format_status = "ok" +def _wrap_article_body(inner_html: str) -> str: + """把排版好的段落包到带固定 CSS 的", f'
') + return f'