feat(compose): 标书docx合成支持章节markdown图片行——下载invoke_model产物真图add_picture嵌入(居中+图注),下载失败保留文字占位诚实降级(2026-09-07用户需求:章节配图用文生图真图非字符画)
This commit is contained in:
parent
ff20d6fa14
commit
3f6790edad
@ -35,11 +35,57 @@ async def _workspace_dir(sor, project_id):
|
||||
return ''
|
||||
|
||||
|
||||
def _fetch_image_bytes(url, timeout=30):
|
||||
"""下载配图(章节正文里的 markdown 图片 URL → 本地字节流)。
|
||||
|
||||
URL 是 invoke_model 产物经 downloadfile2url 落地的本地持久地址
|
||||
(/idfile 静态路径,上游 24h 时效 URL 已在推理层落地,这里拿到的是
|
||||
平台自身地址)。http(s) 下载;本地绝对路径直读。失败返回 None(诚实
|
||||
降级:docx 里保留文字说明,不假装成功)。
|
||||
"""
|
||||
try:
|
||||
u = (url or '').strip()
|
||||
if not u:
|
||||
return None
|
||||
if u.startswith('http://') or u.startswith('https://'):
|
||||
import urllib.request
|
||||
req = urllib.request.Request(u, headers={'User-Agent': 'pipeline-bidding'})
|
||||
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
||||
data = resp.read()
|
||||
return data if data else None
|
||||
if u.startswith('/') and os.path.isfile(u):
|
||||
with open(u, 'rb') as f:
|
||||
return f.read()
|
||||
except Exception as e:
|
||||
logger.warning("compose: 配图下载失败 url=%s: %s", (url or '')[:120], e)
|
||||
return None
|
||||
|
||||
|
||||
# markdown 图片行:(章节写作者按角色 prompt 规范嵌入 invoke_model 产物)
|
||||
_MD_IMG_RE = None
|
||||
|
||||
|
||||
def _parse_md_image(line):
|
||||
"""解析一行 markdown 图片语法,返回 (alt, url) 或 None。"""
|
||||
global _MD_IMG_RE
|
||||
if _MD_IMG_RE is None:
|
||||
import re
|
||||
_MD_IMG_RE = re.compile(r'^\s*!\[([^\]]*)\]\(([^)\s]+)\)\s*$')
|
||||
m = _MD_IMG_RE.match(line or '')
|
||||
if not m:
|
||||
return None
|
||||
return m.group(1), m.group(2)
|
||||
|
||||
|
||||
def _write_docx(path, title, subtitle, chapters):
|
||||
"""用 python-docx 写标书。返回 (ok, page_hint_or_err)。"""
|
||||
"""用 python-docx 写标书。返回 (ok, page_hint_or_err)。
|
||||
|
||||
章节正文支持 markdown 图片行 (写作者经 invoke_model 生成的
|
||||
真实配图):下载后 add_picture 嵌入;下载失败保留文字占位(诚实降级)。
|
||||
"""
|
||||
try:
|
||||
from docx import Document
|
||||
from docx.shared import Pt
|
||||
from docx.shared import Pt, Inches
|
||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||
except ImportError:
|
||||
return False, "python-docx 未安装"
|
||||
@ -72,12 +118,35 @@ def _write_docx(path, title, subtitle, chapters):
|
||||
doc.add_paragraph("%s %s" % (c.get("chapter_no", ""), c.get("title", "")))
|
||||
doc.add_page_break()
|
||||
|
||||
img_ok = img_fail = 0
|
||||
for c in chapters:
|
||||
doc.add_heading("%s %s" % (c.get("chapter_no", ""), c.get("title", "")), level=1)
|
||||
for line in (c.get("content") or "").split("\n"):
|
||||
t = line.rstrip()
|
||||
if not t:
|
||||
continue
|
||||
img = _parse_md_image(t)
|
||||
if img:
|
||||
alt, url = img
|
||||
data = _fetch_image_bytes(url)
|
||||
if data:
|
||||
import io
|
||||
pic_p = doc.add_paragraph()
|
||||
pic_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
try:
|
||||
pic_p.add_run().add_picture(io.BytesIO(data),
|
||||
width=Inches(5.5))
|
||||
if alt:
|
||||
cap = doc.add_paragraph(alt)
|
||||
cap.alignment = WD_ALIGN_PARAGRAPH.CENTER
|
||||
img_ok += 1
|
||||
except Exception as e:
|
||||
doc.add_paragraph("[配图 %s 嵌入失败:%s]" % (alt or url[:60], str(e)[:80]))
|
||||
img_fail += 1
|
||||
else:
|
||||
doc.add_paragraph("[配图缺失(下载失败):%s %s]" % (alt or "", url[:80]))
|
||||
img_fail += 1
|
||||
continue
|
||||
if t.startswith("### "):
|
||||
doc.add_heading(t[4:], level=3)
|
||||
elif t.startswith("## "):
|
||||
@ -90,6 +159,8 @@ def _write_docx(path, title, subtitle, chapters):
|
||||
doc.add_paragraph(t)
|
||||
doc.add_page_break()
|
||||
doc.save(path)
|
||||
if img_ok or img_fail:
|
||||
logger.info("compose: docx 配图嵌入 成功%d 失败%d", img_ok, img_fail)
|
||||
return True, ""
|
||||
except Exception as e:
|
||||
return False, "%s: %s" % (type(e).__name__, str(e)[:200])
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user