React Design Draft
根据内容自动生成可编辑的React风格信息图设计稿,支持单图与多图模式。
基于真实视频素材,自动创建暗黑风格的TikTok/Reels激励视频。
openclaw skills install @thuongvh2-python/tiktok-motivation-video命令、参数、文件名以原文为准
30-90 秒竖版(9:16)黑暗风格励志视频 —— 模仿 @vuongmilano 风格。
制作流程:脚本生成(Claude)+ 语音合成(ElevenLabs)+ 真实视频素材(Pexels API)+ 文字动画叠加(MoviePy)+ 色彩调色(FFmpeg)
重要提示:所有视觉素材均来自 Pexels API(真实拍摄的库存视频)。
禁止使用 FAL.ai、Replicate 或任何 AI 图像生成工具。仅使用 Pexels。
系统提示生成包含台词、时间点及 Pexels 搜索关键词的 JSON 数据。
系统提示:
你是一位擅长创作黑暗美学 TikTok 励志内容的顶尖文案写手。
风格参考 @vuongmilano:短句有力,节奏紧凑,富有哲思。
氛围黑暗、电影感强、诗意浓烈。直击那些孤独却奋进、渴望突破自我的人。
规则:最多 8-15 行;每行不超过 8 个词;杜绝冗余;每个词都掷地有声。
先制造张力,再释放爆发力;结尾必须留下一句令人难忘的收尾。
使用 [PAUSE] 标记以营造戏剧性停顿。
仅返回有效 JSON(不包含 markdown 代码块标记):
{
"title": "...",
"lines": [
{"id": 1, "text": "...", "duration_ms": 3000, "pause_after_ms": 500},
{"id": 2, "text": "[PAUSE]", "duration_ms": 1200, "pause_after_ms": 0}
],
"total_duration_ms": 60000,
"hashtags": ["#darkmotivation", "#discipline", "#fyp"],
"pexels_search_queries": ["dark city rain night", "lone person walking", "shadow dramatic"]
}import json
import requests
def generate_script(topic, tone, duration, api_key):
r = requests.post(
"https://api.anthropic.com/v1/messages",
headers={
"x-api-key": api_key,
"anthropic-version": "2023-06-01",
"content-type": "application/json"
},
json={
"model": "claude-sonnet-4-20250514",
"max_tokens": 1000,
"system": SCRIPT_SYSTEM,
"messages": [
{"role": "user", "content": f"Topic: {topic}\nTone: {tone}\nDuration: {duration}s"}
]
}
)
raw = r.json()["content"][0]["text"]
return json.loads(raw.replace("```json", "").replace("```", "").strip())语音设置适用于黑暗励志风格:
稳定性:0.75,相似度增强:0.85,风格:0.30,启用说话人增强
模型 ID:eleven_multilingual_v2
推荐语音:
Adam pNInz6obpgDQGcFmaJgB 低沉、权威
Antoni ErXwobaYiN019PkySvjV 温暖、强烈
Josh TxGEqnHWrfWFTfGW9XjX 电影感、戏剧化
from pydub import AudioSegment
import io
def generate_voiceover(script, voice_id, api_key, out="voice.mp3"):
segs = []
for line in script["lines"]:
if line["text"] == "[PAUSE]":
segs.append(AudioSegment.silent(duration=line["duration_ms"]))
continue
r = requests.post(
f"https://api.elevenlabs.io/v1/text-to-speech/{voice_id}",
headers={
"xi-api-key": api_key,
"Content-Type": "application/json"
},
json={
"text": line["text"],
"model_id": "eleven_multilingual_v2",
"voice_settings": {
"stability": 0.75,
"similarity_boost": 0.85,
"style": 0.30,
"use_speaker_boost": True
}
}
)
segs.append(AudioSegment.from_mp3(io.BytesIO(r.content)))
if line.get("pause_after_ms", 0) > 0:
segs.append(AudioSegment.silent(duration=line["pause_after_ms"]))
final = sum(segs)
final.export(out, format="mp3")
return out, len(final)免费 API 密钥获取地址:https://www.pexels.com/api/
按主题默认搜索关键词:
THEME_QUERIES = {
"dark city rain": [
"dark city rain night",
"rain street cinematic",
"dark alley bokeh",
"night city slow motion"
],
"lone warrior": [
"lone person silhouette night",
"person walking alone dark",
"shadow warrior",
"lone figure darkness"
],
"dark nature": [
"dark forest fog cinematic",
"storm clouds dramatic",
"night mountain silhouette",
"dark ocean storm"
],
"minimal shadow": [
"shadow dark minimal",
"dark room single light",
"dramatic low key lighting",
"black shadow person"
]
}import os
def fetch_video_clips(theme, duration, pexels_key, output_dir="clips", script=None):
os.makedirs(output_dir, exist_ok=True)
# 使用 Claude 生成的关键词,若无则使用默认主题
queries = (script or {}).get("pexels_search_queries") or THEME_QUERIES.get(theme, THEME_QUERIES["dark city rain"])
needed = max(3, duration // 15)
collected = []
for query in queries:
if len(collected) >= needed:
break
print(f" Pexels: 搜索 '{query}'")
r = requests.get(
"https://api.pexels.com/videos/search",
headers={"Authorization": pexels_key},
params={"query": query, "per_page": 5, "orientation": "portrait", "size": "medium"}
)
for video in r.json().get("videos", []):
if len(collected) >= needed:
break
# 选择宽度 ≥720px 的 MP4 文件
files = sorted(video.get("video_files", []), key=lambda f: f.get("width", 0), reverse=True)
url = next((f["link"] for f in files if f.get("width",0)>=720 and "mp4" in f.get("file_type","")), None)
if not url:
continue
path = os.path.join(output_dir, f"clip_{len(collected):02d}.mp4")
with open(path, "wb") as f:
for chunk in requests.get(url, stream=True, timeout=60).iter_content(8192):
f.write(chunk)
collected.append({
"path": path,
"duration": video.get("duration", 10),
"photographer": video.get("user", {}).get("name", "Pexels")
})
print(f" 已下载片段 {len(collected)}/{needed}")
if not collected:
raise RuntimeError("未获取到任何视频片段。请检查 PEXELS_API_KEY 是否正确。")
return collectedPexels 注意事项:
orientation=portrait 返回竖屏适配的视频片段将所有视频片段裁剪为 1080x1920 竖屏格式。叠加暗色图层。文字与语音时间轴同步。
字体:从 fonts.google.com 下载 Bebas Neue 字体 → 保存至 fonts/BebasNeue-Regular.ttf
from moviepy.editor import (
VideoFileClip, AudioFileClip, ColorClip, TextClip,
concatenate_videoclips, CompositeVideoClip, CompositeAudioClip
)
def crop_to_vertical(clip, w=1080, h=1920):
clip = clip.resize(height=h)
if clip.w < w:
clip = clip.resize(width=w)
return clip.crop(x_center=clip.w/2, width=w).resize((w, h))
def compose_video(clips_info, audio_path, script, out="output.mp4", bg_music_path=None):
audio = AudioFileClip(audio_path)
total_dur = audio.duration
# 从 Pexels 片段中拼接背景
pool = clips_info * 4
parts, remaining = [], total_dur
for info in pool:
if remaining <= 0: break
try:
c = crop_to_vertical(VideoFileClip(info["path"], audio=False))
dur = min(c.duration - 0.3, remaining)
if dur <= 0: continue
parts.append(c.subclip(0, dur))
remaining -= dur
except: continue
if not parts:
raise RuntimeError("无法加载任何视频片段")
bg = concatenate_videoclips(parts, method="compose").set_duration(total_dur)
# 暗色图层 —— 营造氛围的关键
dark = ColorClip((1080,1920), color=(0,0,0)).set_opacity(0.55).set_duration(total_dur)
# 文字内容按脚本时间点同步显示
texts = []
t = 0.0
for line in script["lines"]:
if line["text"] == "[PAUSE]":
t += line["duration_ms"] / 1000
continue
dur = line["duration_ms"] / 1000
try:
clip = TextClip(line["text"], fontsize=78, font="fonts/BebasNeue-Regular.ttf",
color="white", stroke_color="black", stroke_width=3,
size=(900, None), method="caption")
except:
clip = TextClip(line["text"], fontsize=78, color="white",
stroke_color="black", stroke_width=3, size=(900,None), method="caption")
texts.append(clip.set_duration(dur).crossfadein(0.25).crossfadeout(0.25).set_start(t).set_position("center"))
t += dur + line.get("pause_after_ms", 0) / 1000
if bg_music_path and os.path.exists(bg_music_path):
mus = AudioFileClip(bg_music_path).volumex(0.08).set_duration(total_dur)
final_audio = CompositeAudioClip([audio, mus])
else:
final_audio = audio
CompositeVideoClip([bg, dark] + texts).set_audio(final_audio).write_videofile(
out, fps=30, codec="libx264", audio_codec="aac", bitrate="8000k", threads=4, preset="fast"
)
return outffmpeg -y -i output.mp4 \
-vf "curves=blue='0/0.05 1/1.1',eq=contrast=1.15:brightness=-0.04:saturation=0.60,vignette=PI/4,noise=alls=6:allf=t+u" \
-c:v libx264 -crf 17 -c:a copy final_video.mp4滤镜说明:
import os
import subprocess
def create_motivation_video(
topic,
tone="dark & stoic",
duration=60,
visual_theme="dark city rain",
voice_id="pNInz6obpgDQGcFmaJgB",
output="motivation_video.mp4",
bg_music_path=None
):
ak = os.getenv("ANTHROPIC_API_KEY")
ek = os.getenv("ELEVENLABS_API_KEY")
pk = os.getenv("PEXELS_API_KEY")
print("[1/5] 生成脚本..."); script = generate_script(topic, tone, duration, ak)
print("[2/5] 生成配音..."); audio, _ = generate_voiceover(script, voice_id, ek)
print("[3/5] 搜索视频片段..."); clips = fetch_video_clips(visual_theme, duration, pk, script=script)
print("[4/5] 合成视频..."); raw = compose_video(clips, audio, script, "raw.mp4", bg_music_path)
print("[5/5] 调色...");
subprocess.run([
"ffmpeg", "-y", "-i", raw, "-vf",
"curves=blue='0/0.05 1/1.1',eq=contrast=1.15:brightness=-0.04:saturation=0.60,vignette=PI/4",
"-c:v", "libx264", "-crf", "17", "-c:a", "copy", output
], check=True)
print(f"\n完成: {output}")
print(f"标签: {' '.join(script['hashtags'])}")
print("字幕: 视频素材来自 Pexels")
return output, scriptpip install requests Pillow moviepy pydub numpy
# macOS: brew install ffmpeg
# Ubuntu: apt install ffmpeg -y
# 字体: fonts.google.com/specimen/Bebas+Neue -> fonts/BebasNeue-Regular.ttf
# .env
ANTHROPIC_API_KEY=sk-ant-...
ELEVENLABS_API_KEY=...
PEXELS_API_KEY=... # 免费注册获取,访问 pexels.com/apipython scripts/pipeline_full.py --topic "自律" --duration 60 --theme "暗城雨夜"无 Pexels 搜索结果 → 扩大关键词范围;检查 API 密钥是否正确
画面为横屏 → crop_to_vertical() 会自动处理
文字与音频不同步 → 确保时间参数单位为秒(原代码中使用 /1000 将毫秒转为秒)
配音单调 → 降低稳定性(stability)至 0.5,提高风格化(style)至 0.5
字体加载失败 → 使用 .ttf 文件的完整绝对路径
FFmpeg 未安装 → 使用 brew install ffmpeg 或 apt install ffmpeg 安装
自律:暗城雨夜、阴影剪影夜景、独自行走的人
孤独:雾中孤影、空荡黑暗房间、深夜独处
奋起:风暴后山巅、黎明前暗天、雾中小径
反派弧线:戏剧性低光阴影、暗烟流动、情绪化人像
奋斗:夜城延时、城市雨夜光斑、暗街灯光
references/pexels_search_guide.md - 按情绪分类的完整关键词列表
references/elevenlabs_voices.md - 配音角色目录
scripts/pipeline_full.py - 完整可运行脚本
已收录 1 个 Skill