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 ''
|
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):
|
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:
|
try:
|
||||||
from docx import Document
|
from docx import Document
|
||||||
from docx.shared import Pt
|
from docx.shared import Pt, Inches
|
||||||
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
from docx.enum.text import WD_ALIGN_PARAGRAPH
|
||||||
except ImportError:
|
except ImportError:
|
||||||
return False, "python-docx 未安装"
|
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_paragraph("%s %s" % (c.get("chapter_no", ""), c.get("title", "")))
|
||||||
doc.add_page_break()
|
doc.add_page_break()
|
||||||
|
|
||||||
|
img_ok = img_fail = 0
|
||||||
for c in chapters:
|
for c in chapters:
|
||||||
doc.add_heading("%s %s" % (c.get("chapter_no", ""), c.get("title", "")), level=1)
|
doc.add_heading("%s %s" % (c.get("chapter_no", ""), c.get("title", "")), level=1)
|
||||||
for line in (c.get("content") or "").split("\n"):
|
for line in (c.get("content") or "").split("\n"):
|
||||||
t = line.rstrip()
|
t = line.rstrip()
|
||||||
if not t:
|
if not t:
|
||||||
continue
|
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("### "):
|
if t.startswith("### "):
|
||||||
doc.add_heading(t[4:], level=3)
|
doc.add_heading(t[4:], level=3)
|
||||||
elif t.startswith("## "):
|
elif t.startswith("## "):
|
||||||
@ -90,6 +159,8 @@ def _write_docx(path, title, subtitle, chapters):
|
|||||||
doc.add_paragraph(t)
|
doc.add_paragraph(t)
|
||||||
doc.add_page_break()
|
doc.add_page_break()
|
||||||
doc.save(path)
|
doc.save(path)
|
||||||
|
if img_ok or img_fail:
|
||||||
|
logger.info("compose: docx 配图嵌入 成功%d 失败%d", img_ok, img_fail)
|
||||||
return True, ""
|
return True, ""
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return False, "%s: %s" % (type(e).__name__, str(e)[:200])
|
return False, "%s: %s" % (type(e).__name__, str(e)[:200])
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user