价格

小红书图文

一句话出小红书图文

将得到:一篇 post.md(5 个标题 + 正文 + 标签)和 3~6 张 3:4 配图,放在工作空间

桌面端需要 Python

可以这样用

  • 推荐一只能装 16 寸电脑的通勤包,我用了三个月
  • 打工人早餐 5 分钟搞定的 6 个做法
  • 三款平价洗面奶实测,油皮视角
安全检测:0 个高危、0 个中危、3 个提示级命中(2026/9/25 20:20:44)

技能说明

【小红书图文】用户在首页点了「一句话出小红书图文」。交付物:工作空间里的 post.md(5 个候选标题 + 正文 + 话题标签) 和 3~6 张 3:4 配图(1080×1440 PNG)。文字和图放在同一个文件夹里,用户点开就能照着发。

【铁律】

  • 不做 AI 改图冒充实拍。 配图只有两种来源:① 用户给的真实照片,裁成 3:4、压暗加字;② 纯文字卡片(封面大字 + 内页要点)。 绝不生成"看起来像实物照片"的图,也不要把用户的产品图 P 成另一个样子 —— 发出去被认出来就是账号的事。
  • 不许写绝对化用语和功效承诺(最好、第一、100%、根治、稳赚这类)。写完必须跑 xhs_lint.py 自检,有硬错就改到零。
  • 用户没提供的事实不要编:没给价格就不写价格,没给参数就不写参数。宁可少写一句。

【工作流】

  1. 用 TodoWrite 列出:想角度 → 写正文 → 自检违禁词 → 出配图 → 交付,五步,不要再细拆。 列完先探一次出图环境(只一条命令,别等到第 5 步才发现缺库): python3 -c "import PIL" 2>&1 | tail -1(Windows 用 py -3)。报 ModuleNotFoundError 就按第 5 步的办法 立刻装上再往下写正文 —— 授权弹窗趁用户还在的时候弹,别让人走开一分钟回来看到一句提问。
  2. 想角度:按用户选的风格定调子 —— 种草="我用了之后解决了什么问题";干货="N 个方法/清单";测评="优点缺点都说,谁适合谁别买"。 用户给了素材文件夹就先用 fs/read 列一遍文件名,心里有数有哪些图能用;给了产品图就以图里真实可见的东西为准。
  3. 写 post.md,结构固定(xhs_lint.py 按这个结构解析,别改标题名):
    • ## 标题(5 选 1):5 个候选,每个 ≤20 字。5 个要真的不一样 —— 痛点式、结果式、数字式、疑问式、反常识式各一个,不要同一句话换词。
    • ## 正文:300800 字(不含空白)。开头两行必须是钩子(说清楚"这篇解决谁的什么问题"),中间分 35 小段、每段一个点, 结尾一句行动引导。小红书正文用短句、多换行,可以用 emoji 但一段最多一个。
    • ## 标签:3~10 个 #话题,大词(#好物分享)和精准词(#通勤包)各一半。
  4. 自检:用 fs/write 把下面这段代码原样写成工作空间里的 xhs_lint.py(一个字符都不要改), 再 exec 跑 python3 xhs_lint.py post.md(Windows 换成 py -3,不要调 python3,那是应用商店的空壳别名)。 退出码 2 表示有硬错:照着它打印的词和长度问题改 post.md 再跑一次,改到退出码 0 或 1 为止(1 是提示,酌情处理)。 最多改 3 轮;3 轮还不过就把剩下的问题如实写在收尾说明里。自检完把 xhs_lint.py 删掉。
import json, re, sys

BANNED = ["最佳","最好","最优","最强","最低价","最便宜","最高级","最先进","最新科技","最新技术","第一品牌","全网第一","销量第一","排名第一","国家级","世界级","顶级","极品","极致","绝无仅有","独一无二","史无前例","前所未有","唯一","万能","100%","百分百","全网最低","永久","一劳永逸","治愈","根治","疗效","抗癌","防癌","消炎","杀菌","抑菌","祛疤","祛斑","生发","瘦身","减肥不反弹","躺瘦","包瘦","美白针","医用级","药用","稳赚","保本","零风险","包赚","暴富","躺赚","月入过万","稳定收益"]
RISKY = ["加微信","微信号","+V","加V","vx","VX","威信","私信我购买","扫码购买","淘宝搜","拼多多搜","闲鱼","点击链接","戳链接","全网首发","免费送","抽奖","代购","刷单","返现","好评返"]

text = open(sys.argv[1], encoding='utf-8').read()

def section(name):
    m = re.search(r'^#{1,4}\s*' + name + r'.*$([\s\S]*?)(?=^#{1,4}\s|\Z)', text, re.M)
    return m.group(1).strip() if m else ''

titles = [re.sub(r'^\s*(?:[-*]|\d+[.、])\s*', '', l).strip()
          for l in section('标题').split('\n') if l.strip()]
titles = [t for t in titles if t]
body = section('正文')
tags = re.findall(r'#([^#\s]{1,20})', section('标签') or text)

hard, soft = [], []
for w in BANNED:
    if w in text:
        hard.append({'kind': 'banned', 'word': w})
for w in RISKY:
    if w in text:
        soft.append({'kind': 'risky', 'word': w})

if len(titles) != 5:
    hard.append({'kind': 'titles', 'detail': '标题要 5 个候选,现在 %d 个' % len(titles)})
for t in titles:
    if len(t) > 20:
        hard.append({'kind': 'title-len', 'detail': '标题超 20 字:%s' % t})

body_len = len(re.sub(r'\s', '', body))
if not 300 <= body_len <= 800:
    hard.append({'kind': 'body-len', 'detail': '正文 %d 字,要求 300~800' % body_len})
if not 3 <= len(tags) <= 10:
    hard.append({'kind': 'tags', 'detail': '话题标签 %d 个,要求 3~10' % len(tags)})

print(json.dumps({'titles': len(titles), 'body_len': body_len, 'tags': len(tags),
                  'hard': hard, 'soft': soft}, ensure_ascii=False, indent=2))
for h in hard:
    print('硬错:' + (h.get('word') or h.get('detail')))
for s in soft:
    print('提示(可能限流):' + s['word'])
sys.exit(2 if hard else (1 if soft else 0))
  1. 出配图:同样把下面这段原样写成 xhs_cards.py,再用 fs/write 写一份 cards.json,跑 python3 xhs_cards.py cards.json .。
    • cards.json 是数组,每项:{"file":"01-cover.png","type":"cover|inner","theme":"warm|clean|dark|green","title":"…","lines":["…"],"footer":"…","photo":"用户图的绝对路径(可选)"}
    • 张数按用户选的来(默认 4 张):1 张封面 + 25 张内页。封面标题就用 5 个候选里最好的那个,可以用 \n 手动断行,每行 ≤10 字; 内页一页一个要点,title 一句话、lines 24 条短句,别把正文整段贴进去。
    • 用户给了实拍图就把绝对路径填进 photo,脚本会裁成 3:4 并压暗顶部;主题色四选一,一组图用同一个 theme 保持统一。
    • 脚本打印 NEED_PILLOW 说明本机没装 Pillow:直接 exec 跑 python3 -m pip install --user pillow(timeout 300000)装上再重跑, 不要再用一句提问把交付打断 —— exec 本身就会弹授权弹窗、把这条命令原样给用户看,用户点允许就是同意(装依赖要经用户同意的规矩由弹窗满足)。 Windows 换成 py -3 -m pip install --user pillow(禁止调 python3,那是应用商店的空壳别名)。 报 No module named pip(精简版 Linux 常见)就先跑一次 python3 -m ensurepip --user 再装。 还是装不上(网络不通、pip 报错)才停:把报错原文和这条命令告诉用户,只交 post.md,在收尾说明里讲清楚图为什么没出。 同一条安装命令最多试 2 次,不要反复重装。
    • 打印 NO_CJK_FONT 说明系统没有中文字体(少见,多为精简版 Linux),这时也只交 post.md,不要硬出一堆方框图。
    • 出图成功后跑一次 python3 -c "from PIL import Image;import glob;print([Image.open(f).size for f in sorted(glob.glob('*.png'))])" 确认每张都是 1080×1440,再把 xhs_cards.py 和 cards.json 删掉。
  2. 交付:post.md 和 PNG 都在工作空间里,直接说文件名。没有 post.md 就不许说「已完成」; 图没出来就如实说卡在哪一步,不要编造。
  3. 收尾说明 6 行内:推荐用哪个标题(一句话说为什么)、正文多少字、几张图、自检结果(有没有剩下的风险词), 最后一句「想换个角度或改哪张图直接说」。
import json, os, sys
try:
    from PIL import Image, ImageDraw, ImageFont, ImageFilter
except ImportError:
    print('NEED_PILLOW: 请先安装 Pillow:python3 -m pip install pillow')
    sys.exit(3)

W, H = 1080, 1440
THEMES = {
    'warm':  ((255, 240, 232), (61, 42, 34), (232, 106, 74)),
    'clean': ((248, 248, 246), (34, 34, 34), (40, 110, 200)),
    'dark':  ((28, 28, 32), (245, 245, 245), (255, 186, 73)),
    'green': ((236, 246, 238), (30, 60, 42), (46, 150, 90)),
}
FONT_DIRS = ['C:/Windows/Fonts', '/System/Library/Fonts', '/System/Library/Fonts/Supplemental',
             '/Library/Fonts', '/usr/share/fonts', os.path.expanduser('~/.fonts')]
CANDIDATES = ['msyhbd.ttc', 'msyh.ttc', 'msyh.ttf', 'simhei.ttf', 'PingFang.ttc', 'Hiragino Sans GB.ttc',
              'STHeiti Medium.ttc', 'NotoSansCJK-Bold.ttc', 'NotoSansCJKsc-Bold.otf',
              'NotoSansCJK-Regular.ttc', 'SourceHanSansSC-Bold.otf', 'wqy-zenhei.ttc', 'DroidSansFallbackFull.ttf']

def find_font():
    for d in FONT_DIRS:
        for root, _, files in os.walk(d) if os.path.isdir(d) else []:
            lower = {f.lower(): f for f in files}
            for c in CANDIDATES:
                if c.lower() in lower:
                    return os.path.join(root, lower[c.lower()])
    return None

FONT_PATH = find_font()
if not FONT_PATH:
    print('NO_CJK_FONT: 系统里没找到中文字体,无法出图')
    sys.exit(4)

def font(size):
    return ImageFont.truetype(FONT_PATH, size)

def wrap(draw, text, f, max_w):
    lines, cur = [], ''
    for ch in text:
        if ch == '\n':
            lines.append(cur); cur = ''; continue
        if draw.textlength(cur + ch, font=f) > max_w and cur:
            lines.append(cur); cur = ch
        else:
            cur += ch
    if cur:
        lines.append(cur)
    return lines

def base_image(card, bg):
    photo = card.get('photo')
    if photo and os.path.exists(photo):
        im = Image.open(photo).convert('RGB')
        scale = max(W / im.width, H / im.height)
        im = im.resize((round(im.width * scale), round(im.height * scale)), Image.LANCZOS)
        im = im.crop(((im.width - W) // 2, (im.height - H) // 2,
                      (im.width - W) // 2 + W, (im.height - H) // 2 + H))
        # 压暗顶部,保证白字可读;不改动照片内容本身
        shade = Image.new('RGBA', (W, H), (0, 0, 0, 0))
        ImageDraw.Draw(shade).rectangle([0, 0, W, int(H * 0.55)], fill=(0, 0, 0, 120))
        return Image.alpha_composite(im.convert('RGBA'), shade).convert('RGB'), True
    return Image.new('RGB', (W, H), bg), False

def render(card, out_dir):
    bg, fg, accent = THEMES.get(card.get('theme', 'warm'), THEMES['warm'])
    im, on_photo = base_image(card, bg)
    if on_photo:
        fg = (255, 255, 255)
    draw = ImageDraw.Draw(im)
    pad = 88
    is_cover = card.get('type') == 'cover'
    title = card.get('title', '')
    tf = font(96 if is_cover else 64)
    y = 180 if is_cover else 150
    for line in wrap(draw, title, tf, W - pad * 2)[:4]:
        draw.text((pad, y), line, font=tf, fill=fg)
        y += tf.size + 24
    if is_cover and not on_photo:
        draw.rectangle([pad, y + 10, pad + 160, y + 24], fill=accent)
        y += 60
    bf = font(46)
    y += 40
    for raw in card.get('lines', []):
        for line in wrap(draw, raw, bf, W - pad * 2):
            if y > H - pad - bf.size:
                break
            draw.text((pad, y), line, font=bf, fill=fg)
            y += bf.size + 20
        y += 16
    footer = card.get('footer')
    if footer:
        ff = font(34)
        draw.text((pad, H - pad - ff.size), footer, font=ff, fill=accent)
    out = os.path.join(out_dir, card['file'])
    im.save(out, 'PNG')
    return out

cards = json.load(open(sys.argv[1], encoding='utf-8'))
out_dir = sys.argv[2] if len(sys.argv) > 2 else '.'
os.makedirs(out_dir, exist_ok=True)
for c in cards:
    print('OK ' + render(c, out_dir))
print('FONT ' + FONT_PATH)

在「不墨ONE」客户端里填两个空就能跑起来,成品直接落到你本地文件夹。

开始使用看看别的智能体