style: optimize summary image rendering
This commit is contained in:
@@ -14,8 +14,7 @@ async def safe_close_browser(browser, timeout: float = 4.0) -> None:
|
||||
if not browser:
|
||||
return
|
||||
|
||||
# 1. 先尝试关闭所有 page(最重要)
|
||||
for context in browser.contexts[:]: # 复制列表防止修改中迭代
|
||||
for context in browser.contexts[:]:
|
||||
for page in context.pages[:]:
|
||||
try:
|
||||
await asyncio.wait_for(page.close(), timeout=1.5)
|
||||
@@ -26,7 +25,6 @@ async def safe_close_browser(browser, timeout: float = 4.0) -> None:
|
||||
except:
|
||||
pass
|
||||
|
||||
# 2. 尝试优雅关闭 browser
|
||||
try:
|
||||
await asyncio.wait_for(browser.close(), timeout=timeout)
|
||||
logger.debug("browser closed gracefully")
|
||||
@@ -34,13 +32,10 @@ async def safe_close_browser(browser, timeout: float = 4.0) -> None:
|
||||
except (asyncio.TimeoutError, Exception) as e:
|
||||
logger.warning(f"browser.close failed: {e}")
|
||||
|
||||
# 3. 强制杀进程树(最关键一步)
|
||||
if browser.process and browser.process.pid:
|
||||
try:
|
||||
parent = psutil.Process(browser.process.pid)
|
||||
children = parent.children(recursive=True)
|
||||
|
||||
# 3.1 try graceful terminate first
|
||||
for proc in children:
|
||||
try:
|
||||
proc.terminate()
|
||||
@@ -50,25 +45,19 @@ async def safe_close_browser(browser, timeout: float = 4.0) -> None:
|
||||
parent.terminate()
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
||||
pass
|
||||
|
||||
# 3.2 wait briefly, then force kill
|
||||
try:
|
||||
gone, alive = psutil.wait_procs([parent] + children, timeout=2)
|
||||
except Exception:
|
||||
gone, alive = [], [parent] + children
|
||||
|
||||
for proc in alive:
|
||||
try:
|
||||
proc.kill()
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
||||
pass
|
||||
|
||||
# 3.3 final wait and report
|
||||
try:
|
||||
gone, alive = psutil.wait_procs([parent] + children, timeout=3)
|
||||
except Exception:
|
||||
alive = []
|
||||
|
||||
if alive:
|
||||
logger.warning(f"process still alive after kill: {[p.pid for p in alive]}")
|
||||
else:
|
||||
@@ -77,39 +66,188 @@ async def safe_close_browser(browser, timeout: float = 4.0) -> None:
|
||||
logger.warning(f"force kill failed: {e}")
|
||||
|
||||
|
||||
# ================= 样式与 HTML 处理 =================
|
||||
|
||||
async def md_str_to_html_content(md_content):
|
||||
"""
|
||||
将 Markdown 字符串转换为 HTML 内容字符串(逻辑保持不变)。
|
||||
"""
|
||||
# 转换 Markdown 为 HTML
|
||||
"""将 Markdown 字符串转换为更有风格的 HTML 内容。"""
|
||||
html_body = markdown.markdown(md_content, extensions=['extra', 'codehilite'])
|
||||
|
||||
# 保持你原有的 CSS 样式不变
|
||||
css = """
|
||||
<style>
|
||||
:root {
|
||||
--bg1: #f6f3ff;
|
||||
--bg2: #eef7ff;
|
||||
--paper: rgba(255,255,255,0.94);
|
||||
--text: #243042;
|
||||
--muted: #64748b;
|
||||
--primary: #6d5efc;
|
||||
--primary-soft: rgba(109,94,252,0.10);
|
||||
--accent: #13b0a5;
|
||||
--line: rgba(148,163,184,0.20);
|
||||
--code-bg: #0f172a;
|
||||
--code-fg: #e2e8f0;
|
||||
--quote-bg: rgba(19,176,165,0.08);
|
||||
--shadow: 0 18px 50px rgba(94, 92, 154, 0.12);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; }
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', 'Noto Sans CJK SC', 'Microsoft YaHei', sans-serif;
|
||||
padding: 20px 25px; line-height: 1.6; max-width: 750px; margin: 0 auto;
|
||||
background-color: #f9f9f9; color: #333; border: 1px solid #f0f0f0; font-size: 16px;
|
||||
color: var(--text);
|
||||
font-size: 16px;
|
||||
line-height: 1.78;
|
||||
background:
|
||||
radial-gradient(circle at top left, #ffffff 0%, var(--bg1) 38%, transparent 70%),
|
||||
radial-gradient(circle at top right, #ffffff 0%, var(--bg2) 34%, transparent 70%),
|
||||
linear-gradient(180deg, #f7f8fc 0%, #eef4fb 100%);
|
||||
padding: 28px;
|
||||
}
|
||||
.wrap {
|
||||
max-width: 820px;
|
||||
margin: 0 auto;
|
||||
background: var(--paper);
|
||||
border: 1px solid rgba(255,255,255,0.7);
|
||||
border-radius: 28px;
|
||||
box-shadow: var(--shadow);
|
||||
overflow: hidden;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
.hero {
|
||||
min-height: 22px;
|
||||
background: linear-gradient(135deg, rgba(109,94,252,0.14), rgba(19,176,165,0.09));
|
||||
border-bottom: 1px solid var(--line);
|
||||
}
|
||||
.content {
|
||||
padding: 24px 34px 34px;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
color: var(--text);
|
||||
margin-top: 24px;
|
||||
margin-bottom: 14px;
|
||||
font-weight: 700;
|
||||
line-height: 1.35;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
h1 {
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 2.08em;
|
||||
text-align: center;
|
||||
color: #1f2557;
|
||||
}
|
||||
h1 + p {
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
color: var(--muted);
|
||||
font-size: 1.02em;
|
||||
}
|
||||
h2 {
|
||||
font-size: 1.42em;
|
||||
margin-top: 30px;
|
||||
padding: 10px 14px;
|
||||
background: linear-gradient(90deg, var(--primary-soft), rgba(255,255,255,0));
|
||||
border-left: 4px solid var(--primary);
|
||||
border-radius: 12px;
|
||||
}
|
||||
h3 {
|
||||
font-size: 1.15em;
|
||||
margin-top: 24px;
|
||||
color: #30435f;
|
||||
padding-left: 12px;
|
||||
border-left: 3px solid rgba(19,176,165,0.55);
|
||||
}
|
||||
p {
|
||||
margin: 14px 0;
|
||||
color: #334155;
|
||||
line-height: 1.88;
|
||||
}
|
||||
ul, ol {
|
||||
padding-left: 26px;
|
||||
margin: 14px 0 18px;
|
||||
}
|
||||
li {
|
||||
margin: 8px 0;
|
||||
color: #334155;
|
||||
}
|
||||
li::marker {
|
||||
color: var(--primary);
|
||||
}
|
||||
strong {
|
||||
color: #1e293b;
|
||||
font-weight: 700;
|
||||
}
|
||||
em {
|
||||
color: #5b6b84;
|
||||
}
|
||||
code {
|
||||
background: rgba(109,94,252,0.08);
|
||||
color: #5b3df5;
|
||||
padding: 2px 8px;
|
||||
border-radius: 8px;
|
||||
font-size: 0.92em;
|
||||
border: 1px solid rgba(109,94,252,0.10);
|
||||
}
|
||||
pre {
|
||||
background: var(--code-bg);
|
||||
color: var(--code-fg);
|
||||
padding: 16px 18px;
|
||||
border-radius: 16px;
|
||||
overflow-x: auto;
|
||||
border: 1px solid rgba(255,255,255,0.06);
|
||||
box-shadow: inset 0 1px 0 rgba(255,255,255,0.03);
|
||||
}
|
||||
pre code {
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
table {
|
||||
border-collapse: separate;
|
||||
border-spacing: 0;
|
||||
width: 100%;
|
||||
margin: 20px 0;
|
||||
background: rgba(255,255,255,0.88);
|
||||
border: 1px solid rgba(148,163,184,0.16);
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 8px 24px rgba(15,23,42,0.05);
|
||||
}
|
||||
th, td {
|
||||
padding: 12px 14px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid rgba(148,163,184,0.12);
|
||||
}
|
||||
tr:last-child td { border-bottom: none; }
|
||||
th {
|
||||
background: linear-gradient(180deg, rgba(109,94,252,0.10), rgba(109,94,252,0.04));
|
||||
color: #334155;
|
||||
font-weight: 700;
|
||||
}
|
||||
blockquote {
|
||||
margin: 18px 0;
|
||||
padding: 14px 18px;
|
||||
background: var(--quote-bg);
|
||||
border: 1px solid rgba(19,176,165,0.16);
|
||||
border-left: 5px solid var(--accent);
|
||||
border-radius: 14px;
|
||||
color: #355468;
|
||||
}
|
||||
hr {
|
||||
border: none;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, transparent, rgba(148,163,184,0.35), transparent);
|
||||
margin: 26px 0;
|
||||
}
|
||||
a {
|
||||
color: var(--primary);
|
||||
text-decoration: none;
|
||||
border-bottom: 1px dashed rgba(109,94,252,0.35);
|
||||
}
|
||||
.signature {
|
||||
margin-top: 34px;
|
||||
text-align: right;
|
||||
color: var(--muted);
|
||||
font-size: 0.95em;
|
||||
font-style: italic;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6 { color: #222; margin-top: 24px; margin-bottom: 16px; font-weight: 600; line-height: 1.3; }
|
||||
h1 { font-size: 2.2em; padding-bottom: 12px; border-bottom: 1px solid #eee; text-align: center; margin-bottom: 25px; color: #1a1a1a; }
|
||||
h2 { font-size: 1.8em; padding-bottom: 10px; margin-top: 30px; border-bottom: 1px solid #eee; color: #2c3e50; }
|
||||
h3 { font-size: 1.5em; margin-top: 25px; padding-left: 12px; border-left: 4px solid #ddd; color: #34495e; }
|
||||
pre, code { background-color: #f5f5f5; padding: 12px; border-radius: 4px; font-family: 'Courier New', Courier, monospace; font-size: 0.95em; border: 1px solid #eee; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 18px 0; background-color: white; }
|
||||
th, td { border: 1px solid #eee; padding: 10px 12px; text-align: left; }
|
||||
th { background-color: #fafafa; font-weight: 600; }
|
||||
p { margin: 16px 0; color: #333; line-height: 1.8; font-size: 16px; }
|
||||
ul, ol { padding-left: 25px; margin: 18px 0; }
|
||||
li { margin: 8px 0; line-height: 1.7; font-size: 16px; }
|
||||
blockquote { margin: 18px 0; padding: 12px 18px; background-color: #f8f8f8; border-left: 5px solid #ddd; color: #555; font-size: 1em; }
|
||||
strong { color: #222; font-weight: 600; }
|
||||
a { color: #3498db; text-decoration: none; }
|
||||
h3 em { color: #fa8c16; font-style: normal; font-size: 1.1em; }
|
||||
.signature { margin-top: 35px; text-align: right; color: #777; font-size: 0.95em; font-style: italic; }
|
||||
</style>
|
||||
"""
|
||||
|
||||
@@ -120,25 +258,24 @@ async def md_str_to_html_content(md_content):
|
||||
{css}
|
||||
</head>
|
||||
<body>
|
||||
{html_body}
|
||||
<div class="wrap">
|
||||
<div class="hero"></div>
|
||||
<div class="content">
|
||||
{html_body}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>'''
|
||||
return full_html
|
||||
|
||||
|
||||
# ================= 浏览器与图片处理 =================
|
||||
|
||||
def check_chromium_installed(path):
|
||||
return os.path.isfile(path) and os.access(path, os.X_OK)
|
||||
|
||||
|
||||
async def html_to_image(html_content, output_image):
|
||||
"""
|
||||
优化版:直接注入 HTML 字符串生成图片。
|
||||
"""
|
||||
async with async_playwright() as p:
|
||||
browser_path = None
|
||||
# 保持你原有的浏览器路径搜索逻辑
|
||||
if os.name == 'nt':
|
||||
possible_chrome_paths = [
|
||||
r"C:\Users\Liu_WIN10\AppData\Local\Google\Chrome\Application\chrome.exe",
|
||||
@@ -160,11 +297,10 @@ async def html_to_image(html_content, output_image):
|
||||
browser_path = path
|
||||
break
|
||||
|
||||
# 启动浏览器,添加关键的稳定性参数
|
||||
launch_args = [
|
||||
"--no-sandbox",
|
||||
"--disable-setuid-sandbox",
|
||||
"--disable-dev-shm-usage" # 解决 Linux 内存共享问题
|
||||
"--disable-dev-shm-usage"
|
||||
]
|
||||
|
||||
if browser_path:
|
||||
@@ -175,34 +311,27 @@ async def html_to_image(html_content, output_image):
|
||||
browser = await p.chromium.launch(args=launch_args)
|
||||
|
||||
try:
|
||||
# 使用更高的 device_scale_factor 可以让图片更清晰
|
||||
context = await browser.new_context(device_scale_factor=1)
|
||||
page = await context.new_page()
|
||||
|
||||
# 3. 动态调整高度:先探测内容实际高度
|
||||
logger.debug("Measure body height")
|
||||
body_height = await page.evaluate("document.body.scrollHeight")
|
||||
await page.set_viewport_size({"width": 750, "height": body_height})
|
||||
# 2. 【关键】强制等待所有字体和 Emoji 加载完成
|
||||
# 很多时候卡住就是在等字体渲染计算
|
||||
|
||||
logger.debug("Wait for fonts ready")
|
||||
await page.evaluate("document.fonts.ready")
|
||||
# 【优化核心】:直接设置 HTML 内容,不走 file:// 协议
|
||||
# 这样可以彻底避免文件读取超时
|
||||
|
||||
logger.debug("Set page content")
|
||||
await page.set_content(html_content, wait_until='load')
|
||||
|
||||
# 稍微等待一下确保 CSS 渲染完成
|
||||
await asyncio.sleep(0.5)
|
||||
|
||||
# 截图(full_page=True 会自动处理高度)
|
||||
# 4. 截图增加超时限制,防止死锁
|
||||
logger.debug(f"Take screenshot: output={output_image}, height={body_height}")
|
||||
await page.screenshot(
|
||||
path=output_image,
|
||||
full_page=True,
|
||||
timeout=30000, # 30秒硬超时
|
||||
animations="disabled" # 禁用可能的 CSS 动画
|
||||
timeout=30000,
|
||||
animations="disabled"
|
||||
)
|
||||
if not os.path.exists(output_image):
|
||||
raise RuntimeError(f"截图失败,输出文件不存在: {output_image}")
|
||||
@@ -212,16 +341,10 @@ async def html_to_image(html_content, output_image):
|
||||
await safe_close_browser(browser)
|
||||
|
||||
|
||||
# ================= 主转换函数 =================
|
||||
|
||||
async def convert_md_str_to_image(md_content: str, output_image: str, max_retries: int = 3) -> str:
|
||||
"""
|
||||
主函数:从字符串转换 Markdown 到图片(异步版)。
|
||||
"""
|
||||
if not md_content:
|
||||
raise ValueError("Markdown content cannot be empty")
|
||||
|
||||
# 路径准备
|
||||
project_root = os.getcwd()
|
||||
temp_dir = Path(project_root) / "temp" / "md2image"
|
||||
temp_dir.mkdir(parents=True, exist_ok=True)
|
||||
@@ -236,13 +359,9 @@ async def convert_md_str_to_image(md_content: str, output_image: str, max_retrie
|
||||
if output_image_path.exists():
|
||||
os.remove(str(output_image_path))
|
||||
|
||||
# 1. 直接获取生成的 HTML 字符串,不再写临时文件
|
||||
full_html = await md_str_to_html_content(md_content)
|
||||
|
||||
# 2. 转换图片
|
||||
await html_to_image(full_html, str(output_image_path))
|
||||
|
||||
# 3. 验证
|
||||
image_size = os.path.getsize(str(output_image_path))
|
||||
if image_size < 1024:
|
||||
raise RuntimeError(f"图片生成异常,大小仅为: {image_size} bytes")
|
||||
@@ -257,58 +376,3 @@ async def convert_md_str_to_image(md_content: str, output_image: str, max_retrie
|
||||
await asyncio.sleep((attempt + 1) * 2)
|
||||
|
||||
raise RuntimeError(f"图片生成失败,已重试 {max_retries} 次。最后错误: {last_error}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 示例 Markdown 字符串(包含中文和 Emoji)
|
||||
md_content = """#🌟「4KED康复训练群 - 05-30 总结」🌟
|
||||
|
||||
## 📊 今日数据快报
|
||||
- **总消息数**:📩 约300条
|
||||
- **最活跃时段**:🔥 09:00-10:00 (📈 50条/小时)
|
||||
- **聊天时段**:🕒 08:28 - 16:16
|
||||
|
||||
## 🌌 话题总结
|
||||
### 1️⃣ 【车辆保险费用上涨】 ⭐⭐⭐⭐⭐
|
||||
🕒 **聊天时段**:11:33 - 13:16 (👥 6人参与)
|
||||
🔍 **话题回顾**:
|
||||
本次讨论围绕 **车辆保险费用上涨** 展开,堪称今日群聊的"流量担当"。一开始,[@Summer✊] 抛出了一个爆炸性问题:"今年车辆保费居然比去年贵",瞬间点燃了大家的热情。随后,[@火鸡味锅巴] 表示支持,提出了 **保险改革导致价格上涨**,认为 **保险公司收益未达预期,保费自然水涨船高**,并举了一个让人信服的例子 **自己的保险从8K+只返了170元**。然而,[@达文西] 却持相反意见,抛出 **可以不买车损险**,强调 **认真开车就能省下大头费用**,还顺手甩出一句调侃"车损是大头"。
|
||||
讨论的高潮出现在 [@啊菜] 的加入,他不仅提出了 **进口车保险确实更贵**,还分享了一段 **奥迪比雷车贵是合理的对比**,让整个话题从抱怨上升到了品牌差异的讨论层面。大家你一言我一语,气氛热烈得像是开了一场线上辩论会!
|
||||
👍 **金句回顾**:"保的少了,保价贵了,主打的就是个减量加价" —— [@火鸡味锅巴]
|
||||
📌 **额外信息**:讨论中提及了 **保险改革和统一保费政策**,有兴趣的可以去深入研究一下。
|
||||
|
||||
### 2️⃣ 【幼儿园六一活动攀比】 ⭐⭐⭐⭐
|
||||
🕒 **聊天时段**:15:17 - 15:25 (👥 5人参与)
|
||||
🔍 **高能讨论**:
|
||||
本话题的火花由 [@暗香] 无意间点燃,他随口提到 **幼儿园六一活动零食大礼包攀比**,没想到立刻引发了一场头脑风暴。[@水牛] 率先下场,详细分析了 **老师组织活动的问题**,从 **统一准备没新意** 到 **自己准备变攀比**,娓娓道来,最后得出一个令人拍案叫绝的结论:"这种事情就是老师不会搞"。紧接着,[@Summer✊] 不甘示弱,掏出了 **幽默建议** 作为佐证,比如 **带两瓶拉菲或者直接带钱把同学东西全买了**,让讨论瞬间变得硬核起来。
|
||||
然而,[@互联网赵括] 却用一贯的幽默风格插话:"带15升哇哈哈",搭配一个搞笑表情"猪头",把严肃的气氛冲淡了不少,引得大家纷纷刷屏"哈哈哈"。
|
||||
📌 **实用干货**:这次聊出了不少好东西,比如推荐了 **编五彩绳作为活动创意**,实测可用,建议收藏!
|
||||
|
||||
### 3️⃣ 【手工制作高达模型的痛苦】 ⭐⭐⭐⭐
|
||||
🕒 **聊天时段**:09:10 - 09:29 (👥 5人参与)
|
||||
🔍 **讨论亮点**:
|
||||
这次讨论围绕 **手工制作高达模型的痛苦经历** 展开,简直是群聊中的一场"思想盛宴"。一开始,大家还在轻松闲聊,但 [@火鸡味锅巴] 突然抛出了一个独特的视角:"深刻体会了胶佬的痛苦,涂不完的热熔胶",瞬间让话题升温。他还详细补充了 **制作过程中的各种困难**,比如 **热熔胶烫手、时间紧迫、还要上色**,逻辑清晰得让人不得不服。
|
||||
随后,[@清风] 表示认同,补充了 **可以优化制作,比如加LED灯光**,并提到自己如果参与必然"大杀四方"。而 [@Summer✊] 则提出了疑问:"你真弄啊",引发了一轮新的讨论。大家围绕 **制作难度** 和 **创意想法** 你来我往,聊得不亦乐乎。
|
||||
👍 **精华总结**:"太不容易了,时间又紧,明年请假得了" —— [@火鸡味锅巴]
|
||||
|
||||
### 4️⃣ 【谈恋爱风险与个性妹子】 ⭐⭐⭐
|
||||
🕒 **聊天时段**:13:39 - 14:00 (👥 5人参与)
|
||||
🔍 **精彩瞬间**:
|
||||
这次讨论的焦点是 **谈恋爱的风险**,一开始只是 [@T T] 的随口一问:"现在的男生要谈个恋爱风险蛮高",没想到却掀起了一波热议。[@互联网赵括] 率先响应,提出了 **有个性的妹子通常不差**,并分享了一个真实案例:"我印象里比较有个性的姑娘不会长得太差",让大家对问题有了更直观的理解。随后,[@火鸡味锅巴] 提出了完全不同的 **观点**,理由是 **何必因为一棵树放弃一片森林**,还顺带调侃了一句:"谈恋爱干嘛,互相满足生理需求不就好了"。
|
||||
讨论中,[@Y] 还搬出了搞笑补充 **榜一大哥的调侃**,试图证明 **恋爱风险确实高**,这让话题从日常闲聊上升到了"情感高度"。虽然最后大家没达成一致,但这场唇枪舌剑真是精彩纷呈!
|
||||
|
||||
### 5️⃣ 【水费欠款离谱事件】 ⭐⭐⭐
|
||||
🕒 **聊天时段**:10:34 - 10:38 (👥 5人参与)
|
||||
🔍 **讨论小结**:
|
||||
相比前面的话题,这次的 **水费欠款事件** 显得轻松不少,但依然趣味横生。话题从 [@雨的回忆] 的一句"买的房子原房东欠了2万多吨水费" 开始,聊着聊着就跑到了 **如何处理欠款的搞笑讨论**。比如,有人提到 **催前房东交钱**,[@互联网赵括] 立马接梗,分享了一段 **调侃原房东可能是干屠宰或发电的**,比如 **"拿来发电我都信"**,笑点密集,群里瞬间刷屏了一堆"哈哈"表情。
|
||||
[@火鸡味锅巴] 还不忘补刀:"欠了多少我也不知道",让这场讨论成了名副其实的"欢乐场"。虽然话题不算深刻,但这种轻松的氛围也让大家放松了不少。
|
||||
|
||||
## 🎖️ 今日荣誉榜
|
||||
🏆 **群聊 MVP**:[@火鸡味锅巴]
|
||||
👑 **获奖理由**:
|
||||
✅ 发起 3 个热门话题,贡献 5 个表情包/段子
|
||||
✅ **创新贡献**:"高达模型制作痛苦心得"(已申请专利 🎉)
|
||||
|
||||
✨ *本总结由 AI 自动生成,快来看看你今天是不是最靓的崽!🔥*"""
|
||||
spath = asyncio.run(convert_md_str_to_image(md_content, "output.png"))
|
||||
print(spath)
|
||||
|
||||
Reference in New Issue
Block a user