📦 一、整體功能定位
這是一個用于從原始視頻自動生成短視頻解說內容的自動化工具,包含:
-
視頻抽幀(可基于畫面變化提取關鍵幀)
-
多模態圖像識別(每幀圖片理解)
-
文案生成(大模型生成口語化解說)
-
TTS語音合成(將文案變為解說音頻)
-
視頻合成(圖片 + 音頻 or 原視頻音頻)
-
日志記錄 + 文案保存
適合用于內容創作、短視頻自動剪輯輔助、圖文轉視頻等場景。
🧱 二、代碼架構總覽
project/
├── main.py # 主流程邏輯入口
├── config.ini # 配置文件(模型URL、key等)
├── doubaoTTS.py # 語音合成模塊(豆包接口)
├── output/ # 輸出目錄(幀圖、文案、音頻、視頻)
│ ├── frames/
│ ├── narration.mp3
│ ├── frame_descriptions.txt
│ ├── narration_script.txt
│ └── final_output_with_original_audio.mp4
?? 三、主要模塊說明
1. extract_keyframes_by_diff()
– 拆幀模塊(圖像變化判斷)
-
從視頻中按設定幀率提取幀
-
對比相鄰幀內容,過濾重復或近似幀
-
降低識圖調用頻率,提高信息效率
?
2. describe_image()
– 圖像識別模塊
-
使用
OLLAMA
的多模態模型(如gemma3:4b
) -
將每幀圖像轉 base64,提交模型識別
-
輸出自然語言描述(中文)
3. generate_script()
– 文案生成模塊
-
使用
OpenRouter
接口,調用如deepseek
模型 -
以所有幀描述為輸入,生成整體解說詞
-
提示詞優化為:短視頻風格 + 口語化 + 幽默
4. synthesize_audio()
– 音頻合成模塊
-
調用
doubaoTTS
實現 TTS 中文語音合成 -
輸出
.mp3
格式音頻文件
5. compose_video_with_audio()
– 視頻合成模塊
-
將圖像序列按順序合成為視頻
-
配上:
-
合成解說音頻,或
-
原視頻音頻(當前采用)
-
6. main()
– 主流程協調函數
執行順序如下:
原始視頻↓
圖像抽幀(基于變化)↓
每幀圖像識別 → 收集所有描述↓
文案生成(整體解說)↓
保存描述 + 文案↓
語音合成↓
合成視頻( 原視頻+音頻)
🛠 四、配置文件說明(config.ini
)
[ollama]
url = http://localhost:11434/api/generate
model = gemma3:4b[openrouter]
url = https://openrouter.ai/api/v1/chat/completions
api_key = sk-xxx
model = deepseek-chat
🔁 五、可擴展方向建議
功能 | 實現思路 |
---|---|
加字幕 | 用 MoviePy 在視頻幀上疊加文案 |
多語言支持 | 替換 TTS 模型或調用多語種 GPT 模型 |
語音識別 | 加入 whisper 抽取原視頻語音作為額外提示輸入 |
圖像字幕識別 | 加 OCR 模塊識別畫面內字幕,輔助理解 |
批量處理視頻 | 使用 CLI 參數或腳本批量遍歷目錄 |
Web 界面化 | 用 Gradio / Streamlit 構建可視化上傳和預覽界面 |
?
直接上代碼:
import os
import requests
import json
import base64
import logging
from PIL import Image
from moviepy.editor import VideoFileClip, ImageSequenceClip, AudioFileClip
import doubaoTTS
import configparser
from PIL import Image, ImageChops
import numpy as np# 設置日志
logging.basicConfig(level=logging.INFO, format='[%(levelname)s] %(message)s')# 從 config.ini 加載配置
config = configparser.ConfigParser()
config.read("config.ini", encoding="utf-8")OLLAMA_URL = config.get("ollama", "url")
OLLAMA_MODEL = config.get("ollama", "model")
OPENROUTER_URL = config.get("openrouter", "url")
OPENROUTER_API_KEY = config.get("openrouter", "api_key")
OPENROUTER_MODEL = config.get("openrouter", "model")# 拆幀函數
def extract_keyframes_by_diff(video_path, output_dir, diff_threshold=30, fps=2, max_frames=None):"""從視頻中提取關鍵幀(基于圖像變化)參數:video_path: 視頻文件路徑output_dir: 輸出幀目錄diff_threshold: 像素差閾值,越小越敏感fps: 初始掃描幀率(例如2即每秒取2幀,再做變化判斷)max_frames: 最大輸出幀數,默認不限制"""os.makedirs(output_dir, exist_ok=True)clip = VideoFileClip(video_path)last_frame = Nonecount = 0logging.info(f"開始按幀提取,初始采樣幀率:{fps},變化閾值:{diff_threshold}")for i, frame in enumerate(clip.iter_frames(fps=fps)):if max_frames and count >= max_frames:breakimg = Image.fromarray(frame)if last_frame:# 灰度差異diff = ImageChops.difference(img.convert('L'), last_frame.convert('L'))diff_score = np.mean(np.array(diff))if diff_score < diff_threshold:continue # 差異太小,跳過當前幀# 保存關鍵幀frame_path = os.path.join(output_dir, f"keyframe_{count:05d}.jpg")img.save(frame_path)last_frame = imgcount += 1logging.info(f"關鍵幀提取完成,共保留 {count} 幀")return sorted([os.path.join(output_dir, f) for f in os.listdir(output_dir) if f.endswith(".jpg")])# 用 gemma3:4b 多模態識圖
def describe_image(img_path):logging.info(f"正在識別圖片內容:{img_path}")with open(img_path, "rb") as f:image_bytes = f.read()image_b64 = base64.b64encode(image_bytes).decode("utf-8")#logging.info(image_b64)payload = {"model": OLLAMA_MODEL,"messages": [{"role": "user","content": "請用中文描述這張圖片的內容,不要輸出markdown格式","images": [image_b64]}],"stream": False}response = requests.post(OLLAMA_URL, json=payload)#logging.info(response.json())result = response.json().get('message', {}).get('content', '').strip()logging.info(f"識別結果:{result}")return result# 用 deepseek 生成中文文案(帶完整驗證與日志)
def generate_script(prompt):logging.info("正在生成視頻文案")try:response = requests.post(OPENROUTER_URL,headers={"Authorization": f"Bearer {OPENROUTER_API_KEY}","Content-Type": "application/json"},json={"model": OPENROUTER_MODEL,"messages": [{"role": "user", "content": prompt}],"stream": False})response.raise_for_status()data = response.json()if "choices" in data and data["choices"]:result = data["choices"][0]["message"]["content"]logging.info(f"記錄生成的文案內容:{result}")return resultlogging.error("API 響應中沒有 choices 字段: %s", data)return "生成失敗,API 響應格式異常。"except requests.RequestException as e:logging.error("API 請求失敗: %s", e)return "生成失敗,請檢查網絡或服務狀態。"# 合成音頻
def synthesize_audio(text, output_path):logging.info("正在合成解說音頻")doubaoTTS.tts_doubao(text, output_path)logging.info(f"音頻保存到:{output_path}")# 合成視頻
def compose_video_with_audio(frame_dir, audio_path, output_path, fps=1):logging.info("正在合成最終視頻")frame_paths = sorted([os.path.join(frame_dir, f) for f in os.listdir(frame_dir) if f.endswith(".jpg")])clip = ImageSequenceClip(frame_paths, fps=fps)audio = AudioFileClip(audio_path)video = clip.set_audio(audio)video.write_videofile(output_path, codec='libx264', audio_codec='aac')logging.info(f"視頻生成成功:{output_path}")def main(video_path, work_dir, fps=1):frame_dir = os.path.join(work_dir, "frames")os.makedirs(frame_dir, exist_ok=True)# 1. 拆幀frame_dir = os.path.join(work_dir, "frames")extract_keyframes_by_diff(video_path, frame_dir, diff_threshold=25, fps=2, max_frames=30)# 2. 多幀識圖 + 容錯處理descriptions = []frame_files = sorted([f for f in os.listdir(frame_dir) if f.lower().endswith(".jpg")])for i, frame_file in enumerate(frame_files):frame_path = os.path.join(frame_dir, frame_file)try:description = describe_image(frame_path)if description:descriptions.append(f"第{i+1}幀:{description}")else:logging.warning(f"幀 {frame_file} 描述為空,已跳過")except Exception as e:logging.error(f"處理幀 {frame_file} 時發生錯誤:{e}")continueif not descriptions:logging.error("沒有任何幀成功識別,流程中止")return# 3. 保存描述到文案文件full_description = "\n".join(descriptions)description_txt_path = os.path.join(work_dir, "frame_descriptions.txt")with open(description_txt_path, "w", encoding="utf-8") as f:f.write(full_description)logging.info(f"幀內容描述已保存到:{description_txt_path}")# 4. 文案生成prompt = ("你是一個擅長寫短視頻解說詞的創作者,現在請根據以下每一幀畫面內容,""用輕松幽默的語氣,生成一段通俗易懂、符合短視頻風格的中文解說,""要求內容簡潔、節奏明快、具有代入感,可以適當加入網絡流行語或比喻,""但不要生硬搞笑,也不要重復描述畫面,請讓內容聽起來像是一個博主在視頻里自然說話:\n"f"{full_description}"
)script = generate_script(prompt)# 5. 保存文案內容script_txt_path = os.path.join(work_dir, "narration_script.txt")with open(script_txt_path, "w", encoding="utf-8") as f:f.write(script)logging.info(f"生成的文案已保存到:{script_txt_path}")# 6. 合成解說音頻audio_path = os.path.join(work_dir, "narration.mp3")synthesize_audio(script, audio_path)# 7. 合成視頻(使用原視頻音頻)logging.info("正在加載原視頻音頻")original_clip = VideoFileClip(video_path)original_audio = original_clip.audioframe_paths = sorted([os.path.join(frame_dir, f) for f in os.listdir(frame_dir) if f.lower().endswith(".jpg")])image_clip = ImageSequenceClip(frame_paths, fps=fps).set_audio(original_audio)output_video_path = os.path.join(work_dir, "final_output_with_original_audio.mp4")image_clip.write_videofile(output_video_path, codec='libx264', audio_codec='aac')logging.info(f"視頻已成功生成:{output_video_path}")# 示例調用
main("2025-05-11_15-12-01_UTC.mp4", "output", fps=1) # 請取消注釋后運行