作者:周弘懿(锦琛)
业务背景
AIGC 短剧、竖屏微短剧、品牌短视频正在成为内容供给侧增长最快的赛道之一。和传统影视工业不同,这类内容的生产节奏极快:一个团队一周要产出几十条甚至上百条成片,每条成片背后又有大量关键帧、分镜、候选素材需要生成和筛选。典型的生产链路可以拆成四段:
- 分镜文案:编剧或运营先写出一段一段的分镜脚本,例如"雨夜,女主撑伞站在便利店门口,暖色灯光打在脸上"。
- 关键帧生图:把分镜文案转成一张张关键帧图片,作为镜头的视觉锚点;有时还要在已有素材上做背景替换、风格迁移、参考图融合。
- 图生视频:把选定的关键帧作为首帧,用图生视频模型生成几秒的运动镜头;文生视频则直接从文案生成概念短片。
- 素材沉淀与检索复用:每天产生的图片、视频素材数量巨大,如果只是堆在对象存储里,下次要"找一段雨夜便利店的空镜"只能靠人肉翻文件夹。真正让素材变成资产的,是多模态内容理解 + 向量检索:给素材自动生成标题、描述、标签,把图片和视频都转成向量入库,之后就能以文搜图、以图搜视频、以文搜视频。
这条链路的业务价值:
- 内容生产提效:分镜文案到关键帧、关键帧到运动镜头都由模型批量完成,把人从重复劳动里解放出来,只保留创意和复核环节。
- 素材资产沉淀:所有生成物带着结构化描述和向量入库,形成可检索、可复用的素材库,而不是一次性消耗品。
- 二次创作检索复用:新剧本里出现"复古咖啡馆"的镜头需求时,先在历史素材库里语义检索一遍,能复用的直接复用,不能复用的再走生成,边际成本持续下降。
技术挑战
如果不借助统一的向量数据库平台,自己从零对接各家生成模型、再拼一套检索系统,会撞上一连串工程痛点:
- 生图/生视频模型接入分散:文生图、图生图、文生视频、图生视频、视频编辑往往是不同的服务、不同的鉴权、不同的返回格式,业务代码里塞满各家 SDK 和 HTTP 客户端,维护成本高。
- 异步任务管理复杂:视频生成是典型的长耗时异步任务,创建只拿到
task_id,要不断轮询查询状态。自己管理任务队列、轮询退避、终态判定、超时兜底很容易写错——比如把客户端本地超时误判成服务端失败并无限重试。 - 内容安全审核:AIGC 生成结果具有非确定性,肖像、品牌、版权都有合规风险,需要在生成环节引入审核与人工复核,不能盲目自动发布。
- 生成素材缺乏统一检索入口:图片存 OSS、视频存 OSS、描述文本存业务库、向量又单独存一套向量引擎,四份数据割裂,检索时要跨系统拼装。
- 多模态检索难、复用靠人工:以文搜图、以图搜视频这类跨模态检索,需要把文本、图片、视频映射到同一向量空间;没有语义检索时,运营找素材靠文件名和记忆,历史素材利用率极低,大量生成物变成沉没成本。
整体流程
整条流水线由四个 Milvus AI Function 串成,按"先生成、后检索"分为两个阶段、五个步骤:
四个 AI Function 的分工:
函数 |
作用 |
AI_IMAGE_EDIT |
图像生成 / 编辑,支持单图编辑和多参考图融合 |
AI_VIDEO_EDIT |
图生视频 / 文生视频,异步任务(video_edit 创建、tasks/describe 轮询) |
AI_MULTI_MODAL_GENERATE |
多模态内容理解,为图片 / 视频自动生成标题、描述、标签 |
AI_EMBEDDING |
写入即向量化,把文本 / 图片 / 视频映射到同一向量空间 |
具体AI Function用法参考官方文档
实战操作
下面按流水线顺序讲解五个步骤的目标与要点;为便于对照,所有可运行代码统一集中在文末的「完整示例代码」一节。
阶段一 · 生成
步骤 1:关键帧生图(AI_IMAGE_EDIT / wan2.7-image)
目标:拿一张分镜参考图(或素材图),按分镜文案做背景替换 / 风格调整,产出一张关键帧图片 URL,作为后续图生视频的首帧。AI_IMAGE_EDIT 同步返回,单图输入用 texts,编辑指令写在 params.prompt。
输入:参考图 URL + 分镜文案(prompt) → 输出:关键帧图片 URL。
步骤 2:图生视频(AI_VIDEO_EDIT / happyhorse-1.1-i2v)
目标:把关键帧作为首帧(media.type=first_frame)生成一段运动镜头。视频生成是异步任务:先 POST /v2/vectordb/ai/video_edit 创建拿到 task_id,再 POST /v2/vectordb/ai/tasks/describe 轮询,直到 task_status 变为 SUCCEEDED(拿到 video_url)或终态 FAILED / CANCELED。
输入:首帧图 + 运镜文案 → 输出:运动镜头 video_url(720P / 5s)。
步骤 3:素材理解,生成标题 / 标签(AI_MULTI_MODAL_GENERATE / qwen3.7-plus)
目标:在素材入库前,用多模态大模型给图片 / 视频生成一句客观的检索描述,作为标题、简介或标签来源。图片用 media_type=image,视频用 media_type=video,prompt 必填。这一步产出的 caption / tags 会随素材一起在步骤 4 入库。
输入:图片 / 视频 URL → 输出:标题、描述、标签。
阶段二 · 检索
步骤 4:多模态向量入库(AI_EMBEDDING 写入 / qwen3-vl-embedding)
目标:建一个"写入即向量化"的多模态素材 Collection(is_multimodal=true、dim=2560)。把图片 URL、视频 URL 连同标题、标签写入,Milvus 会自动为素材生成 2560 维向量并落库。图片和视频共享同一向量字段和同一模型,才能在同一空间里跨模态检索。
输入:步骤 1~3 产出的 url + caption + tags → 输出:落库的 2560 维向量。
步骤 5:内容搜索——以文搜图 / 以文搜视频 / 以图搜视频(AI_EMBEDDING 查询)
目标:素材入库后,用同一个多模态模型把查询(文本或图片 URL)映射到同一向量空间,检索最相似的素材实现复用。由于 Collection 绑定了 qwen3-vl-embedding 的 Function,search 的 data 里直接传原始文本或媒体 URL,Milvus 会自动向量化查询。把查询文案换成一张图片 URL,同一个 search 调用即可完成"以图搜图 / 以图搜视频";去掉 filter 则在图片和视频混合库里跨模态检索。
输入:查询文本 / 查询图片 URL → 输出:Top-K 最相似素材(可用 filter 限定模态)。
完整示例代码
将MILVUS_HOST和MILVUS_TOKEN替换成自己的集群就可以直接看到效果
"""AIGC 短剧「生成 → 检索」一体化流水线(阿里云 Milvus AI Function) 步骤1 关键帧生图 → 步骤2 图生视频 → 步骤3 素材理解 → 步骤4 向量入库 → 步骤5 内容搜索 用到四个 AI Function:AI_IMAGE_EDIT / AI_VIDEO_EDIT / AI_MULTI_MODAL_GENERATE / AI_EMBEDDING """ from __future__ import annotations import json import time from typing import Any from urllib.error import HTTPError from urllib.request import Request, urlopen from pymilvus import DataType, Function, FunctionType, MilvusClient # ==== 全局配置(REST 与 gRPC 同在 19530 端口)==== MILVUS_ENDPOINT = "c-xxx.milvus.aliyuncs.com:19530" MILVUS_TOKEN = "root:xxx" MILVUS_REST_BASE_URL = f"http://{MILVUS_ENDPOINT}" MILVUS_URI = MILVUS_REST_BASE_URL def post_json(path: str, body: dict[str, Any], timeout: int = 200) -> tuple[int, dict[str, Any]]: """步骤 1~3 共用的 REST 调用工具。""" request = Request( f"{MILVUS_REST_BASE_URL.rstrip('/')}{path}", data=json.dumps(body, ensure_ascii=False).encode("utf-8"), headers={"Authorization": f"Bearer {MILVUS_TOKEN}", "Content-Type": "application/json"}, method="POST", ) try: with urlopen(request, timeout=timeout) as response: return response.status, json.loads(response.read().decode("utf-8")) except HTTPError as exc: return exc.code, json.loads(exc.read().decode("utf-8")) # ========== 步骤 1:关键帧生图 AI_IMAGE_EDIT (wan2.7-image) 【对应正文·步骤1|代码第 37–55 行】 IMAGE_URL = "https://dashscope.oss-cn-beijing.aliyuncs.com/images/dog_and_girl.jpeg" PROMPT = ( "雨夜,一位女子撑着伞站在便利店门口,暖色的店内灯光打在她脸上," "电影级构图,保留画面主体。" ) status, data = post_json( "/v2/vectordb/ai/image_edit", { "model_name": "wan2.7-image", "texts": [IMAGE_URL], "params": {"prompt": PROMPT, "n": 1, "size": "1024*1024", "watermark": False, "timeout_sec": 180}, }, ) assert status == 200 and data.get("code") == 0, data keyframe_url = data["data"]["output"]["outputs"][0] print("\n" + "=" * 64) print("步骤 1 | AI_IMAGE_EDIT 关键帧生图(wan2.7-image)") print("=" * 64) print(f"关键帧图片 URL:{keyframe_url}") # ========== 步骤 2:图生视频 AI_VIDEO_EDIT (happyhorse-1.1-i2v),异步任务 【对应正文·步骤2|代码第 56–91 行】 VIDEO_MODEL = "happyhorse-1.1-i2v" # 创建与查询必须使用同一模型名 # 用步骤1的关键帧作首帧(media.type=first_frame) status, data = post_json( "/v2/vectordb/ai/video_edit", { "model_name": VIDEO_MODEL, "prompt": "镜头缓缓推进,雨丝飘落,暖光在伞面上轻轻闪动," "保留首帧图中的主体。", "media": [{"type": "first_frame", "url": keyframe_url}], "params": {"resolution": "720P", "audio_setting": "none", "watermark": False, "timeout_sec": 180}, }, ) assert status == 200 and data.get("code") == 0, data task_id = data["data"]["output"]["task_id"] print("\n" + "=" * 64) print("步骤 2 | AI_VIDEO_EDIT 图生视频(happyhorse-1.1-i2v,异步任务)") print("=" * 64) print(f"任务已创建 task_id = {task_id}") # 轮询查询:tasks/describe 一次只查一个 task_id,带 provider 与相同 model_name video_url = None for attempt in range(60): # 本地轮询上限,达到上限只记录 task_id,不判定服务端失败 status, data = post_json( "/v2/vectordb/ai/tasks/describe", {"provider": "aliyun_milvus", "model_name": VIDEO_MODEL, "task_id": task_id}, ) task_status = data.get("data", {}).get("output", {}).get("task_status") print(f" 轮询 #{attempt:<2} -> {task_status}") if task_status == "SUCCEEDED": video_url = data["data"]["output"]["video_url"] break if task_status in ("FAILED", "CANCELED"): raise RuntimeError(f"task ended in {task_status}: {json.dumps(data, ensure_ascii=False)}") time.sleep(10) assert video_url, f"轮询达到本地上限,请稍后凭 task_id={task_id} 继续查询" print(f"运动镜头 video_url:{video_url}") # ========== 步骤 3:素材理解 AI_MULTI_MODAL_GENERATE (qwen3.7-plus) 【对应正文·步骤3|代码第 92–117 行】 def describe_media(url: str, media_type: str, prompt: str) -> str: status, data = post_json( "/v2/vectordb/ai/multi_modal_generate", { "model_name": "qwen3.7-plus", "texts": [url], "params": {"media_type": media_type, "prompt": prompt, "temperature": 0}, }, ) assert status == 200 and data.get("code") == 0, data return data["data"]["output"]["outputs"][0] image_caption = describe_media( keyframe_url, "image", "用一句中文客观描述这张关键帧图片的主体、场景和色调,便于以文搜图。", ) video_caption = describe_media( video_url, "video", "用一句中文客观描述这段视频的主体、动作和画面氛围,便于按镜头检索。", ) print("\n" + "=" * 64) print("步骤 3 | AI_MULTI_MODAL_GENERATE 素材理解(qwen3.7-plus)") print("=" * 64) print(f"[图片描述] {image_caption}") print(f"[视频描述] {video_caption}") # ========== 步骤 4:建多模态 Collection 并向量入库 AI_EMBEDDING (qwen3-vl-embedding) 【对应正文·步骤4|代码第 118–168 行】 EMBED_MODEL = "qwen3-vl-embedding" VECTOR_DIM = 2560 # qwen3-vl-embedding 多模态维度,向量字段维度必须与 dim 一致 client = MilvusClient(uri=MILVUS_URI, token=MILVUS_TOKEN) collection_name = "aigc_assets_multimodal" if client.has_collection(collection_name): client.drop_collection(collection_name) schema = MilvusClient.create_schema(auto_id=True, enable_dynamic_field=False) schema.add_field("id", DataType.INT64, is_primary=True) schema.add_field("media_type", DataType.VARCHAR, max_length=16) # image / video schema.add_field("media_ref", DataType.VARCHAR, max_length=4096) # 用于向量化的图片/视频 URL schema.add_field("caption", DataType.VARCHAR, max_length=2048) # 步骤3 生成的描述 schema.add_field("tags", DataType.VARCHAR, max_length=512) schema.add_field("embedding", DataType.FLOAT_VECTOR, dim=VECTOR_DIM) # 写入 media_ref 时自动调用 qwen3-vl-embedding 生成 2560 维向量 schema.add_function( Function( name="embed_media", function_type=FunctionType.TEXTEMBEDDING, input_field_names=["media_ref"], output_field_names=["embedding"], params={ "provider": "aliyun_milvus", "model_name": EMBED_MODEL, "dim": VECTOR_DIM, "is_multimodal": "true", }, ) ) index_params = client.prepare_index_params() index_params.add_index(field_name="embedding", index_type="AUTOINDEX", metric_type="COSINE") client.create_collection(collection_name=collection_name, schema=schema, index_params=index_params) # 把步骤1~3 的产出(url + caption + tags)向量化入库 client.insert( collection_name, [ {"media_type": "image", "media_ref": keyframe_url, "caption": image_caption, "tags": "关键帧,生成图"}, {"media_type": "video", "media_ref": video_url, "caption": video_caption, "tags": "运动镜头,生成视频"}, ], ) client.flush(collection_name) client.load_collection(collection_name) print("\n" + "=" * 64) print("步骤 4 | AI_EMBEDDING 多模态向量入库(qwen3-vl-embedding, 2560 维)") print("=" * 64) print(f"图 + 视频已向量化入库,Collection:{collection_name}") # ========== 步骤 5:内容搜索 以文搜图 / 以文搜视频 / 以图搜视频 【对应正文·步骤5|代码第 169–196 行】 def search_assets(query: str, top_k: int = 5, media_type: str | None = None): """query 可以是一段中文/英文文案,也可以是一张查询图片的 URL。""" filter_expr = f'media_type == "{media_type}"' if media_type else "" results = client.search( collection_name=collection_name, data=[query], # 文本或图片 URL,均由 qwen3-vl-embedding 向量化 anns_field="embedding", limit=top_k, filter=filter_expr, output_fields=["media_type", "media_ref", "caption", "tags"], ) for rank, hit in enumerate(results[0], 1): e = hit["entity"] url = e["media_ref"].split("?")[0] # 去掉 OSS 签名参数,展示更干净 print(f" {rank}. [相似度 {hit['distance']:.4f}] [{e['media_type']}] {e['caption'][:42]}") print(f" {url}") print("\n" + "=" * 64) print("步骤 5 | 多模态内容搜索(qwen3-vl-embedding 统一向量空间)") print("=" * 64) # 以文搜图:限定只搜图片 print("[5.1] 以文搜图:") search_assets("室内暖光下的条纹毛衣造型", media_type="image") # 以文搜视频:限定只搜视频 print("\n[5.2] 以文搜视频:") search_assets("角色近景,镜头缓慢推进的概念片", media_type="video") # 以图搜视频:用一张关键帧图片 URL 去召回风格相近的视频素材 print("\n[5.3] 以图搜视频(用关键帧图召回同源视频):") search_assets(keyframe_url, media_type="video")
实战效果
依次跑通 入库 → 检索 → 重排等步骤。下图是一次完整运行的真实控制台输出:
上图是这条流水线一次完整跑通的输出:从一张分镜参考图出发,AI_IMAGE_EDIT 产出关键帧图片 URL,AI_VIDEO_EDIT 以关键帧为首帧异步生成运动镜头 video_url,AI_MULTI_MODAL_GENERATE 为图片与视频各生成一句检索描述,随后连同 URL、标签一起写入多模态 Collection(写入即向量化),最后以文搜图 / 以文搜视频 / 以图搜视频三路检索都能召回刚入库的素材并带出相似度分数。
这条「生成 → 检索」链路之所以能一次跑通、且结果可复用,关键在三点:
- 生成即入库:关键帧、运动镜头连同标题、描述、标签一起落库,每个生成物都带着结构化信息和向量,而不是用完即弃的一次性产物。
- 同一向量空间:图片和视频共享同一个 qwen3-vl-embedding 模型与同一向量字段,才能在一个 Collection 里做以文搜图、以图搜视频这类跨模态检索。
- 写入即向量化、查询即推理:应用侧无需自己调 embedding,写入时 Collection Function 自动生成 2560 维向量,search 里直接传原始文本或媒体 URL 即可完成召回。
与自己从零对接各家生成模型、再拼一套检索系统相比,简化非常直观:
维度 |
传统自建方案 |
阿里云 Milvus 一站式 |
生成模型接入 |
文生图 / 图生图 / 文生视频各家 SDK、鉴权、返回格式分散对接 |
AI Function 统一封装生图 / 生视频 / 内容理解 |
异步任务管理 |
自建任务队列、轮询退避、终态判定与超时兜底 |
tasks/describe 标准轮询接口,终态判定清晰 |
素材理解 |
自建或外接标题、描述、标签服务 |
AI_MULTI_MODAL_GENERATE 内置多模态理解 |
向量化链路 |
应用侧先调 embedding 再写入 |
写入即向量化(Collection Function 自动生成) |
检索入口 |
图片 / 视频 / 文本 / 向量四份数据割裂,跨系统拼装 |
单库单次 search 跨模态检索 |
总结
本文用四个 Milvus AI Function 把 AIGC 短剧生产的完整链路串成一条「生成 → 检索」流水线:
- 生成:
AI_IMAGE_EDIT把分镜文案变成关键帧;AI_VIDEO_EDIT用创建 +tasks/describe轮询的异步模型把关键帧变成运动镜头;AI_MULTI_MODAL_GENERATE让每个素材自带标题和描述。 - 检索:
AI_EMBEDDING(多模态qwen3-vl-embedding)把图片和视频统一映射到同一向量空间,写入即向量化、查询即推理,实现以文搜图、以图搜视频、以文搜视频的跨模态检索。
它的价值不只是"生成快",更在于把每一次生成都沉淀为可检索、可复用的素材资产,让内容团队从一次性消耗走向资产化经营。
让 AI 落地,不必再从复杂链路开始
阿里云 Milvus 是目前唯一支持 AI Native 的 Milvus 云服务。它不仅提供高性能、全托管的向量检索能力,更将 AI Function 与 AI-Gateway 原生嵌入数据链路,让模型能力真正成为数据库的一部分。
从数据进入阿里云Milvus 的那一刻起,理解、加工、检索和生成便可以在一条原生链路中完成。阿里云 Milvus 正在把“接入 AI”变成“原生拥有 AI”。