AI 短视频的教程大多停留在"用什么工具"。但对开发者来说,真正的难点是如何把脚本、配音、画面、字幕、合成串成一条稳定可复现的流水线,让每次产出都可追溯、可回滚、可批量。本文从工程视角拆解这条流水线,并给出可运行代码。
核心原则:每个阶段产出物都落盘并记录元数据,任何一步失败都能从中间重跑,而不是从头再来。
用 dataclass 定义项目结构,把"素材"和"元数据"分离。
from dataclasses import dataclass, field, asdict
from enum import Enum
from pathlib import Path
from typing import Any
import json, hashlib, time
class Stage(str, Enum):
script = "script"
assets = "assets"
assemble = "assemble"
render = "render"
gate = "gate"
@dataclass
class Segment:
idx: int
text: str
visual_prompt: str
duration: float
audio: str | None = None
visual: str | None = None
@dataclass
class Project:
project_id: str
title: str
segments: list[Segment] = field(default_factory=list)
stage: Stage = Stage.script
meta: dict[str, Any] = field(default_factory=dict)
created_at: float = field(default_factory=time.time)
def save(self, root: Path) -> Path:
p = root / f"{self.project_id}.json"
p.write_text(json.dumps(asdict(self), ensure_ascii=False, indent=2),
encoding="utf-8")
return p
@staticmethod
def load(path: Path) -> "Project":
data = json.loads(path.read_text(encoding="utf-8"))
data["segments"] = [Segment(**s) for s in data["segments"]]
data["stage"] = Stage(data["stage"])
return Project(**data)项目 JSON 既是状态记录,也是断点续跑的入口。任何阶段崩溃,都可以从上一个完成的阶段继续。
自由文本无法编排。让模型输出可校验 JSON,并强制约束时长和数量。
import os
from openai import OpenAI
from pydantic import BaseModel, Field
client = OpenAI(
api_key=os.getenv("OPENAI_API_KEY"),
base_url=os.getenv("OPENAI_BASE_URL"),
)
class Shot(BaseModel):
idx: int
text: str = Field(max_length=80, description="旁白或台词")
visual: str = Field(max_length=200, description="画面描述")
duration: float = Field(ge=1, le=8)
class Script(BaseModel):
title: str
shots: list[Shot] = Field(min_length=4, max_length=12)
SYSTEM = """你是短视频编剧。生成 6 个镜头,每镜 3-5 秒。
只输出 JSON:{title, shots:[{idx,text,visual,duration}]}。
禁止侵权、违法、虚假宣传内容。"""
def gen_script(topic: str) -> Script:
resp = client.chat.completions.create(
model=os.getenv("OPENAI_MODEL", "gpt-4o-mini"),
messages=[
{"role": "system", "content": SYSTEM},
{"role": "user", "content": f"主题:{topic}"},
],
response_format={"type": "json_object"},
temperature=0.8,
)
return Script.model_validate_json(resp.choices[0].message.content)关键约束:max_length 限制文本长度,min_length/max_length 限制镜头数量,ge/le 限制时长。Schema 越严格,后期返工越少。
素材生成是流水线中最慢、最贵的环节。要点是并行 + 幂等 + 缓存。
import asyncio, hashlib
def asset_key(project_id: str, idx: int, kind: str, prompt: str) -> str:
raw = f"{project_id}:{idx}:{kind}:{prompt}"
return hashlib.sha256(raw.encode()).hexdigest()[:16]
async def gen_visual(prompt: str, out: Path, seed: int) -> str:
# 实际调用即梦、可灵、Stable Diffusion 等官方 API
# 固定 seed、模型、采样器,保证角色一致
await asyncio.sleep(0.1)
return str(out)
async def gen_audio(text: str, out: Path, voice: str) -> str:
# 实际调用 TTS;声音克隆需获得本人授权
await asyncio.sleep(0.05)
return str(out)
async def build_assets(project: Project, root: Path,
concurrency: int = 3) -> None:
sem = asyncio.Semaphore(concurrency)
cache: dict[str, str] = {}
async def one(seg: Segment):
async with sem:
vkey = asset_key(project.project_id, seg.idx, "visual",
seg.visual_prompt)
if vkey in cache:
seg.visual = cache[vkey]
return
vout = root / f"visual_{seg.idx:03d}.png"
seg.visual = await gen_visual(seg.visual_prompt, vout,
seed=42 + seg.idx)
cache[vkey] = seg.visual
aout = root / f"audio_{seg.idx:03d}.mp3"
seg.audio = await gen_audio(seg.text, aout, voice="default")
await asyncio.gather(*(one(s) for s in project.segments))幂等的价值:同一项目重跑时,已生成的素材直接复用,不重复扣费。生产环境应把 cache 换成 Redis 或数据库。
字幕必须和音频时长对齐,否则画面和声音会错位。
from pathlib import Path
def fmt_ts(t: float) -> str:
h, m, s = int(t // 3600), int(t % 3600 // 60), t % 60
return f"{h:02d}:{m:02d}:{s:06.3f}".replace(".", ",")
def make_srt(segments: list[Segment], out: Path) -> Path:
lines, cur = [], 0.0
for i, seg in enumerate(segments, 1):
end = cur + seg.duration
lines.append(f"{i}\n{fmt_ts(cur)} --> {fmt_ts(end)}\n{seg.text}\n")
cur = end
out.write_text("\n".join(lines), encoding="utf-8")
return out时间轴要以实际音频时长为准,而不是脚本里的预估时长。生产环境应读取音频真实时长后重新计算。
import asyncio
async def run_cmd(cmd: list[str]) -> tuple[int, str]:
proc = await asyncio.create_subprocess_exec(
*cmd, stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
_, err = await proc.communicate()
return proc.returncode, err.decode(errors="ignore")[-1500:]
async def concat(parts: list[Path], out: Path) -> None:
listing = out.with_suffix(".txt")
listing.write_text(
"\n".join(f"file '{p.resolve()}'" for p in parts),
encoding="utf-8",
)
code, err = await run_cmd([
"ffmpeg", "-y", "-f", "concat", "-safe", "0",
"-i", str(listing), "-c", "copy", str(out),
])
if code != 0:
raise RuntimeError(f"concat failed: {err}")
async def burn_subtitles(video: Path, srt: Path, out: Path) -> None:
code, err = await run_cmd([
"ffmpeg", "-y", "-i", str(video),
"-vf", f"subtitles={srt}", str(out),
])
if code != 0:
raise RuntimeError(f"subtitle failed: {err}")每个 ffmpeg 命令都要检查返回码,失败时抛出异常,让流水线在上层决定重试还是告警。
import re
BAD = {"违法", "暴力", "色情", "歧视", "虚假", "最好", "第一"}
PII = re.compile(r"(\d{11}|\d{17}[\dXx]|[\w.+-]+@[\w-]+\.[\w.]+)")
def audit_text(text: str) -> None:
if any(w in text for w in BAD):
raise ValueError("含敏感或绝对化用语")
if PII.search(text):
raise ValueError("含个人信息,需脱敏")
def audit_project(p: Project) -> list[str]:
issues = []
if not p.meta.get("bgm_license"):
issues.append("缺少 BGM 授权")
if not p.meta.get("font_license"):
issues.append("缺少字体授权")
if p.meta.get("voice_clone") and not p.meta.get("voice_license"):
issues.append("声音克隆缺授权")
if p.meta.get("has_real_person") and not p.meta.get("portrait_license"):
issues.append("真人肖像缺授权")
return issues
def quality_check(video: Path) -> dict:
size_mb = video.stat().st_size / 1024 / 1024 if video.exists() else 0
return {
"exists": video.exists(),
"size_mb": round(size_mb, 2),
"ok": video.exists() and size_mb > 0.1,
}门禁不通过就不能进入发布环节。AI 生成内容还要按平台要求标注。
AI 自媒体短视频的工程化核心,不是接入多少工具,而是把生产拆成可落盘、可复现、可恢复的阶段:结构化脚本、并行素材、精确时间轴、ffmpeg 合成、质量门禁。代码可以简单,但幂等、留痕、审核、脱敏和授权确认不能省。先跑通单条视频的完整流水线,再做批量、模板化和多形式扩展。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。