
Byted Util Video Editor
- 12 installs
- 411 repo stars
- Updated August 4, 2026
- bytedance/agentkit-samples
video-editor is a Claude skill that uses Python and ffmpeg to merge videos, add and mix audio, and generate or burn subtitles.
About
video-editor is a Python and ffmpeg based video post-production skill. It merges multiple video clips, adds narration or background-music audio tracks, mixes multiple audio files on a timeline, generates SRT subtitle files and burns subtitles into video. A developer uses it via scripts/video_editor.py with modes like merge, add-audio, merge-audios, generate-srt and burn-subtitles. It requires ffmpeg to be installed.
- Merges multiple videos and mixes audio tracks with ffmpeg
- Generates SRT subtitles and burns them into video
- Five operation modes via scripts/video_editor.py
Byted Util Video Editor by the numbers
- 12 all-time installs (skills.sh)
- Ranked #1,038 of 1,335 Generative Media skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
byted-util-video-editor capabilities & compatibility
- Capabilities
- video editing · audio mixing · subtitle generation · video merge
- Use cases
- video generation · transcription
- Platforms
- macOS · Linux
- Pricing
- Free
What byted-util-video-editor says it does
python scripts/video_editor.py output.mp4 --videos video1.mp4 video2.mp4 video3.mp4
需要 ffmpeg 编译时包含 libass(subtitles 滤镜)支持
npx skills add https://github.com/bytedance/agentkit-samples --skill byted-util-video-editorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 12 |
|---|---|
| repo stars | ★ 411 |
| Last updated | August 4, 2026 |
| Repository | bytedance/agentkit-samples ↗ |
What it does
Merge clips, mix audio and add subtitles to produce a finished video with ffmpeg.
Who is it for?
Assembling short videos, promos or tutorial videos from clips and audio.
Skip if: Environments where ffmpeg cannot be compiled with libass for subtitle burning.
When should I use this skill?
You need to merge videos, add audio tracks, mix audio or add subtitles.
What you get
A finished MP4 with merged clips, mixed audio and optional burned-in subtitles.
By the numbers
- Five operation modes (merge, add-audio, merge-audios, generate-srt, burn-subtitles)
- Volume adjustable 0.0-2.0
Files
Video Editor - 视频编辑器
适用场景
当需要进行视频后期处理时使用此技能,包括:
- 合并多个视频:将多个分镜视频合成一个完整视频
- 添加音频:为视频添加旁白、背景音乐等音频轨道
- 合并音频:将多个音频文件按时间轴合并成一个完整音频
- 生成字幕:生成 SRT 格式的字幕文件
- 烧录字幕:将字幕硬编码到视频中
- 视频制作:短视频、宣传片、教学视频的后期合成
功能特性
1. 合并多个视频
- 支持 2+ 个视频文件拼接
- 自动处理视频编码和格式转换
- 保持原始视频质量
- 自动保留原始视频的背景音乐
2. 添加音频轨道
- 支持添加旁白配音
- 支持添加背景音乐
- 可调节音频音量(0.0-2.0 倍)
- 可调节原始视频背景音乐音量(0.0-2.0 倍)
- 背景音乐和对白音频自动混合
3. 合并多个音频
- 支持将多个音频文件按时间轴合并成一个完整音频
- 每个音频可以指定自己的开始时间(秒)
- 使用 amix 滤镜平滑混合音频
4. 生成字幕
- 支持生成标准 SRT 格式字幕文件
- 支持多段字幕,每段有独立的开始/结束时间
- UTF-8 编码,支持中文字幕
5. 烧录字幕
- 将 SRT 字幕硬编码到视频中
- 需要 ffmpeg 编译时包含 libass(subtitles 滤镜)支持
- 如当前环境不支持,可使用其他视频编辑工具(如剪映等)烧录字幕
6. 灵活的操作模式
- merge 模式:合并多个视频(可同时添加音频)
- add-audio 模式:为现有视频添加音频
- merge-audios 模式:合并多个音频文件
- generate-srt 模式:生成 SRT 字幕文件
- burn-subtitles 模式:烧录字幕到视频
系统要求
安装 ffmpeg
Ubuntu/Debian:
sudo apt install ffmpegmacOS:
brew install ffmpeg验证安装:
ffmpeg -version使用步骤
模式 1:合并多个视频
基本用法:
python scripts/video_editor.py output.mp4 --videos video1.mp4 video2.mp4 video3.mp4带音频合并:
python scripts/video_editor.py output.mp4 --videos v1.mp4 v2.mp4 v3.mp4 --audio narration.mp3调整音频音量:
# 对白音频音量为 0.8 倍(降低 20%)
python scripts/video_editor.py output.mp4 --videos v1.mp4 v2.mp4 --audio dialogue.mp3 --volume 0.8
# 对白音频音量为 1.5 倍(提高 50%)
python scripts/video_editor.py output.mp4 --videos v1.mp4 v2.mp4 --audio dialogue.mp3 --volume 1.5
# 同时调整背景音乐音量为 0.5 倍
python scripts/video_editor.py output.mp4 --videos v1.mp4 v2.mp4 --audio dialogue.mp3 --volume 0.8 --bg-volume 0.5模式 2:为现有视频添加音频
基本用法:
python scripts/video_editor.py output.mp4 --video input.mp4 --audio narration.mp3 --mode add-audio调节音量:
python scripts/video_editor.py output.mp4 --video input.mp4 --audio background.mp3 --volume 0.5 --mode add-audio模式 3:合并多个音频文件
基本用法:
python scripts/video_editor.py output.mp3 --audios audio1.mp3,audio2.mp3 --start-times 0,5 --mode merge-audios参数说明:
--audios:要合并的音频文件列表,用逗号分隔--start-times:每个音频的开始时间(秒),用逗号分隔- 两个列表的长度必须相同
模式 4:生成 SRT 字幕文件
基本用法:
python scripts/video_editor.py output.srt --subtitles '[{"text":"你好","start_time":0,"end_time":2},{"text":"世界","start_time":2.5,"end_time":5}]' --mode generate-srt参数说明:
--subtitles:JSON 格式的字幕数据,每个字幕包含:text:字幕文字start_time:开始时间(秒)end_time:结束时间(秒)
模式 5:烧录字幕到视频
基本用法:
python scripts/video_editor.py output_with_subs.mp4 --video input.mp4 --subtitle subs.srt --mode burn-subtitles注意:
- 字幕样式参数已预留,但实际效果取决于 ffmpeg 的 subtitles 滤镜配置
- 如当前环境不支持 subtitles 滤镜,建议:
1. 使用专业视频编辑软件(Final Cut Pro、Premiere、剪映等) 2. 使用在线视频工具(如 Kapwing、Clideo 等)
参数说明
| 参数 | 说明 | 示例 |
|---|---|---|
output | 输出文件路径 | final_video.mp4 |
--videos | 要合并的视频文件列表(2+ 个) | --videos v1.mp4 v2.mp4 v3.mp4 |
--video | 单个视频文件(add-audio 或 burn-subtitles 模式) | --video input.mp4 |
--audio | 单个音频文件(旁白、对白等) | --audio narration.mp3 |
--audios | 要合并的音频文件列表(逗号分隔) | --audios a1.mp3,a2.mp3 |
--start-times | 每个音频的开始时间(逗号分隔,秒) | --start-times 0,5 |
--volume | 对白音频音量(0.0-2.0,默认 1.0) | --volume 0.8 |
--bg-volume | 背景音乐音量(0.0-2.0,默认 1.0) | --bg-volume 0.5 |
--subtitles | 字幕数据(JSON 格式,generate-srt 模式) | --subtitles '[{\"text\":\"你好\",\"start_time\":0,\"end_time\":2}]' |
--subtitle | SRT 字幕文件路径(burn-subtitles 模式) | --subtitle subs.srt |
--font-name | 字幕字体名称 | --font-name "Arial" |
--font-size | 字幕字体大小 | --font-size 24 |
--font-color | 字幕字体颜色 | --font-color "&H00FFFFFF" |
--outline-color | 字幕边框颜色 | --outline-color "&H00000000" |
--outline | 字幕边框宽度 | --outline 2 |
--shadow | 字幕阴影深度 | --shadow 0 |
--alignment | 字幕对齐方式 | --alignment 2 |
--mode | 操作模式:merge、add-audio、merge-audios、generate-srt、burn-subtitles | --mode generate-srt |
实际示例
示例 1:AI 短剧合成
假设有 3 个分镜视频:
shot1.mp4(5秒)shot2.mp4(10秒)shot3.mp4(10秒)
合成无旁白版本:
python scripts/video_editor.py drama_no_audio.mp4 --videos shot1.mp4 shot2.mp4 shot3.mp4合成带旁白版本:
python scripts/video_editor.py drama_with_audio.mp4 --videos shot1.mp4 shot2.mp4 shot3.mp4 --audio narration.mp3示例 2:添加背景音乐
为现有视频添加背景音乐,降低音量:
python scripts/video_editor.py video_with_music.mp4 --video original.mp4 --audio bgm.mp3 --volume 0.4 --mode add-audio示例 3:合并多个对白音频
假设有 3 个对白音频文件,分别在不同时间点播放:
line1.mp3(在 0 秒开始)line2.mp3(在 5 秒开始)line3.mp3(在 12 秒开始)
合并成一个完整音频:
python scripts/video_editor.py full_narration.mp3 --audios line1.mp3,line2.mp3,line3.mp3 --start-times 0,5,12 --mode merge-audios示例 4:生成 SRT 字幕文件
假设有两段对白:
- "你好世界"(0-2 秒)
- "欢迎来到漫剧"(2.5-5 秒)
\\生成字幕文件:
python scripts/video_editor.py drama_subs.srt --subtitles '[{"text":"你好世界","start_time":0,"end_time":2},{"text":"欢迎来到漫剧","start_time":2.5,"end_time":5}]' --mode generate-srt示例 5:烧录字幕到视频
使用默认样式烧录:
python scripts/video_editor.py drama_with_subs.mp4 --video drama.mp4 --subtitle drama_subs.srt --mode burn-subtitles注意: 如环境不支持烧录功能,请直接使用生成的 SRT 字幕文件,配合剪映、Final Cut Pro 等视频编辑软件完成字幕烧录!
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
#!/usr/bin/env python3
# Copyright (c) 2025 Beijing Volcano Engine Technology Co., Ltd. and/or its affiliates.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Video Editor - 视频编辑器
支持功能:
1. 合并多个视频文件
2. 添加音频轨道(旁白、背景音乐等)
3. 视频和音频同步
4. 生成并烧录字幕(SRT格式)
5. 灵活的参数配置
"""
import argparse
import subprocess
import sys
import os
import tempfile
import shutil
from pathlib import Path
import json
def check_ffmpeg():
"""检查 ffmpeg 是否可用"""
try:
result = subprocess.run(["ffmpeg", "-version"], capture_output=True, text=True)
if result.returncode == 0:
return True
return False
except FileNotFoundError:
return False
def install_ffmpeg():
"""提示安装 ffmpeg"""
print("=" * 60)
print("❌ 未找到 ffmpeg")
print("=" * 60)
print("📦 请安装 ffmpeg:")
print(" Ubuntu/Debian: sudo apt install ffmpeg")
print(" macOS: brew install ffmpeg")
print("=" * 60)
return False
def get_video_info(video_path):
"""获取视频信息"""
try:
cmd = [
"ffprobe",
"-v",
"error",
"-show_entries",
"format=duration",
"-show_entries",
"stream=codec_type,codec_name,duration",
"-of",
"json",
str(video_path),
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
return json.loads(result.stdout)
return None
except Exception as e:
print(f"⚠️ 无法获取视频信息:{e}")
return None
def merge_videos(videos, output_file, audio_file=None, audio_volume=1.0, bg_volume=1.0):
"""
合并多个视频文件,保留原始视频的背景音乐
Args:
videos: 视频文件列表
output_file: 输出文件路径
audio_file: 音频文件路径(可选,用于添加对白)
audio_volume: 对白音频音量(0.0-2.0,默认 1.0)
bg_volume: 背景音乐音量(0.0-2.0,默认 1.0)
"""
print("=" * 60)
print("🎬 视频合成开始")
print("=" * 60)
# 检查所有视频文件
valid_videos = []
for i, video in enumerate(videos):
if not os.path.exists(video):
print(f"❌ 视频文件不存在:{video}")
continue
valid_videos.append(video)
# 显示视频信息
info = get_video_info(video)
if info:
duration = info["format"].get("duration", "未知")
print(f" 视频 {i + 1}: {Path(video).name} ({duration}s)")
if not valid_videos:
print("❌ 没有有效的视频文件")
return False
num_videos = len(valid_videos)
print(f"\n✅ 共 {num_videos} 个视频文件")
# 第一步:先合并所有视频(保留原始音频)
temp_merged = None
try:
# 创建临时文件用于合并
temp_dir = tempfile.mkdtemp()
concat_file = os.path.join(temp_dir, "concat.txt")
# 写入 concat 文件
with open(concat_file, "w", encoding="utf-8") as f:
for video in valid_videos:
abs_path = os.path.abspath(video)
# 在 Windows 上需要转义路径
if sys.platform == "win32":
abs_path = abs_path.replace("\\", "/")
f.write(f"file '{abs_path}'\n")
# 使用 concat demuxer 合并视频(保留原始音频)
temp_merged = os.path.join(temp_dir, "temp_merged.mp4")
cmd_merge = [
"ffmpeg",
"-f",
"concat",
"-safe",
"0",
"-i",
concat_file,
"-c",
"copy",
"-y",
temp_merged,
]
print("\n⏳ 正在合并视频和背景音乐...")
result_merge = subprocess.run(cmd_merge, capture_output=True, text=True)
if result_merge.returncode != 0:
print("⚠️ 快速合并失败,尝试重新编码...")
# 如果 copy 失败,尝试重新编码
cmd_merge_reencode = [
"ffmpeg",
"-f",
"concat",
"-safe",
"0",
"-i",
concat_file,
"-c:v",
"libx264",
"-preset",
"medium",
"-crf",
"23",
"-c:a",
"aac",
"-b:a",
"192k",
"-y",
temp_merged,
]
result_merge = subprocess.run(
cmd_merge_reencode, capture_output=True, text=True
)
if result_merge.returncode != 0:
print(f"❌ 视频合并失败:{result_merge.stderr}")
return False
print(f"✅ 背景音乐音量:{bg_volume:.1f}x")
# 第二步:添加对白音频(如果有)
if audio_file and os.path.exists(audio_file):
print(f"✅ 添加对白音频:{Path(audio_file).name}")
print(f" 音量:{audio_volume:.1f}x")
# 使用 add_audio_to_video 来混合背景音乐和对白
# 我们需要调整 add_audio_to_video 的逻辑,或者直接在这里处理
# 先调整背景音乐音量,然后混合
cmd_final = ["ffmpeg", "-i", temp_merged]
if audio_file:
cmd_final.extend(["-i", str(audio_file)])
filters = []
# 调整背景音乐音量
filters.append(f"[0:a]volume={bg_volume}[bg]")
# 如果有对白音频
if audio_file:
filters.append(f"[1:a]volume={audio_volume}[dialogue]")
filters.append(
"[bg][dialogue]amix=inputs=2:duration=first:dropout_transition=0[outa]"
)
filter_complex = ";".join(filters)
cmd_final.extend(
["-filter_complex", filter_complex, "-map", "0:v", "-map", "[outa]"]
)
else:
filter_complex = ";".join(filters)
cmd_final.extend(
["-filter_complex", filter_complex, "-map", "0:v", "-map", "[bg]"]
)
# 编码选项
cmd_final.extend(
[
"-c:v",
"libx264",
"-preset",
"medium",
"-crf",
"23",
"-c:a",
"aac",
"-b:a",
"192k",
"-y",
str(output_file),
]
)
print(f"\n🔧 命令:{' '.join(cmd_final[:5])}... (已简化)")
print("\n⏳ 正在混合音频...")
result_final = subprocess.run(cmd_final, capture_output=True, text=True)
if result_final.returncode != 0:
print(f"❌ 音频混合失败:{result_final.stderr}")
return False
else:
# 只有背景音乐,调整音量
cmd_final = [
"ffmpeg",
"-i",
temp_merged,
"-filter:a",
f"volume={bg_volume}",
"-c:v",
"copy",
"-c:a",
"aac",
"-b:a",
"192k",
"-y",
str(output_file),
]
print(f"\n🔧 命令:{' '.join(cmd_final[:5])}... (已简化)")
print("\n⏳ 正在调整背景音乐音量...")
result_final = subprocess.run(cmd_final, capture_output=True, text=True)
if result_final.returncode != 0:
print(f"❌ 音量调整失败:{result_final.stderr}")
return False
# 清理临时文件
try:
shutil.rmtree(temp_dir)
except Exception:
pass
# 显示结果
if os.path.exists(output_file):
file_size = os.path.getsize(output_file) / 1024 / 1024 # MB
# 获取输出视频信息
output_info = get_video_info(output_file)
duration = "未知"
if output_info:
duration = output_info["format"].get("duration", "未知")
print("\n" + "=" * 60)
print("✅ 视频合成成功!")
print("=" * 60)
print(f"📁 输出文件:{os.path.abspath(output_file)}")
print(f"⏱️ 视频时长:{duration}秒")
print(f"💾 文件大小:{file_size:.1f} MB")
print("=" * 60)
return True
else:
print("\n" + "=" * 60)
print("❌ 视频合成失败")
print("=" * 60)
return False
except Exception as e:
print(f"\n❌ 执行失败:{e}")
return False
def merge_audios(audio_files_with_timings, output_file):
"""
将多个音频文件按时间轴合并成一个完整的音频文件
Args:
audio_files_with_timings: 列表,每个元素是 (audio_path, start_time_seconds) 的元组
output_file: 输出音频文件路径
"""
print("=" * 60)
print("🎵 音频合并开始")
print("=" * 60)
# 检查所有音频文件
valid_audio_files = []
for audio_path, start_time in audio_files_with_timings:
if not os.path.exists(audio_path):
print(f"❌ 音频文件不存在:{audio_path}")
continue
valid_audio_files.append((audio_path, start_time))
print(f" 音频:{Path(audio_path).name}(开始时间:{start_time}s)")
if not valid_audio_files:
print("❌ 没有有效的音频文件")
return False
num_audios = len(valid_audio_files)
print(f"\n✅ 共 {num_audios} 个音频文件")
# 构建 ffmpeg 命令,使用 amix 和 adelay 滤镜
cmd = ["ffmpeg"]
# 添加所有输入音频
filter_parts = []
for i, (audio_path, start_time) in enumerate(valid_audio_files):
cmd.extend(["-i", str(audio_path)])
delay_ms = int(start_time * 1000)
filter_parts.append(f"[{i}:a]adelay={delay_ms}|{delay_ms}[a{i}]")
# 合并所有音频
all_audio_tags = "".join([f"[a{i}]" for i in range(len(valid_audio_files))])
filter_parts.append(
f"{all_audio_tags}amix=inputs={len(valid_audio_files)}:dropout_transition=0[outa]"
)
filter_complex = ";".join(filter_parts)
cmd.extend(
[
"-filter_complex",
filter_complex,
"-map",
"[outa]",
"-c:a",
"libmp3lame",
"-b:a",
"192k",
"-y",
str(output_file),
]
)
print(f"\n🔧 命令:{' '.join(cmd[:5])}... (已简化)")
print("\n⏳ 正在合并音频...")
try:
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0 and os.path.exists(output_file):
file_size = os.path.getsize(output_file) / 1024 / 1024 # MB
print("\n" + "=" * 60)
print("✅ 音频合并成功!")
print("=" * 60)
print(f"📁 输出文件:{os.path.abspath(output_file)}")
print(f"💾 文件大小:{file_size:.1f} MB")
print("=" * 60)
return True
else:
print("\n" + "=" * 60)
print("❌ 音频合并失败")
print("=" * 60)
if result.stderr:
print(f"错误信息:{result.stderr}")
return False
except Exception as e:
print(f"\n❌ 执行失败:{e}")
return False
def add_audio_to_video(video_file, audio_file, output_file, audio_volume=1.0):
"""
为视频添加音频轨道(旁白、背景音乐等)
Args:
video_file: 视频文件路径
audio_file: 音频文件路径
output_file: 输出文件路径
audio_volume: 音频音量(0.0-2.0,默认 1.0)
"""
print("=" * 60)
print("🎵 添加音频到视频")
print("=" * 60)
# 检查文件
if not os.path.exists(video_file):
print(f"❌ 视频文件不存在:{video_file}")
return False
if not os.path.exists(audio_file):
print(f"❌ 音频文件不存在:{audio_file}")
return False
print(f"📹 视频:{Path(video_file).name}")
print(f"🎵 音频:{Path(audio_file).name}")
print(f"🔊 音量:{audio_volume:.1f}x")
# 构建 ffmpeg 命令
cmd = [
"ffmpeg",
"-i",
str(video_file),
"-i",
str(audio_file),
"-c:v",
"copy", # 复制视频流(不重新编码)
"-c:a",
"aac",
"-b:a",
"192k",
"-filter_complex",
f"[1:a]volume={audio_volume}[outa]",
"-map",
"0:v",
"-map",
"[outa]",
"-shortest", # 使用最短流的时长
"-y",
str(output_file),
]
print("\n⏳ 正在添加音频...")
try:
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0 and os.path.exists(output_file):
file_size = os.path.getsize(output_file) / 1024 / 1024 # MB
print("\n" + "=" * 60)
print("✅ 音频添加成功!")
print("=" * 60)
print(f"📁 输出文件:{os.path.abspath(output_file)}")
print(f"💾 文件大小:{file_size:.1f} MB")
print("=" * 60)
return True
else:
print("\n❌ 音频添加失败")
if result.stderr:
print(f"错误信息:{result.stderr}")
return False
except Exception as e:
print(f"\n❌ 执行失败:{e}")
return False
def format_srt_time(seconds):
"""
将秒数格式化为 SRT 时间格式: HH:MM:SS,mmm
Args:
seconds: 秒数(可以是浮点数)
Returns:
SRT 格式的时间字符串
"""
hours = int(seconds // 3600)
minutes = int((seconds % 3600) // 60)
secs = int(seconds % 60)
millis = int((seconds * 1000) % 1000)
return f"{hours:02d}:{minutes:02d}:{secs:02d},{millis:03d}"
def generate_srt(subtitles, output_file):
"""
生成 SRT 字幕文件
Args:
subtitles: 字幕列表,每个元素是字典:
{
'text': '字幕文字',
'start_time': 开始时间(秒),
'end_time': 结束时间(秒)
}
output_file: 输出 SRT 文件路径
"""
print("=" * 60)
print("📝 生成 SRT 字幕")
print("=" * 60)
try:
with open(output_file, "w", encoding="utf-8") as f:
for i, sub in enumerate(subtitles, 1):
f.write(f"{i}\n")
f.write(
f"{format_srt_time(sub['start_time'])} --> {format_srt_time(sub['end_time'])}\n"
)
f.write(f"{sub['text']}\n\n")
print(
f" 字幕 {i}: {sub['text']} ({sub['start_time']}s - {sub['end_time']}s)"
)
print("\n✅ SRT 字幕文件生成成功!")
print(f"📁 输出文件:{os.path.abspath(output_file)}")
print("=" * 60)
return True
except Exception as e:
print(f"\n❌ SRT 字幕生成失败:{e}")
print("=" * 60)
return False
def burn_subtitles_to_video(
video_file,
subtitle_file,
output_file,
font_name="Arial",
font_size=24,
primary_color="white",
outline_color="black",
outline=2,
shadow=0,
alignment=2,
):
"""
将字幕烧录到视频中(硬字幕)
Args:
video_file: 视频文件路径
subtitle_file: SRT 字幕文件路径
output_file: 输出视频文件路径
font_name: 字体名称,默认 "Arial"
font_size: 字体大小,默认 24
primary_color: 主颜色,默认 "white"
outline_color: 边框颜色,默认 "black"
outline: 边框宽度,默认 2
shadow: 阴影深度,默认 0(无阴影)
alignment: 对齐方式,1=左下,2=中下,3=右下,8=左上,9=中上,10=右上,默认 2
"""
print("=" * 60)
print("🎬 烧录字幕到视频")
print("=" * 60)
# 检查文件
if not os.path.exists(video_file):
print(f"❌ 视频文件不存在:{video_file}")
return False
if not os.path.exists(subtitle_file):
print(f"❌ 字幕文件不存在:{subtitle_file}")
return False
print(f"📹 视频:{Path(video_file).name}")
print(f"📝 字幕:{Path(subtitle_file).name}")
# 将字幕文件复制到当前工作目录,避免路径问题
work_dir = os.getcwd()
temp_sub = os.path.join(work_dir, "temp_sub.srt")
try:
shutil.copy(subtitle_file, temp_sub)
sub_name = "temp_sub.srt"
except Exception as e:
print(f"⚠️ 复制字幕文件失败,尝试直接使用原文件:{e}")
temp_sub = subtitle_file
sub_name = os.path.basename(subtitle_file)
# 确保字幕文件在当前工作目录
if os.path.dirname(sub_name):
try:
shutil.copy(subtitle_file, sub_name)
except BaseException:
pass
# 方法1: 尝试使用 ffmpeg 的 subtitles 滤镜
print("\n⏳ 尝试使用 ffmpeg subtitles 滤镜...")
success = False
try:
# 构建 ffmpeg 命令
cmd = [
"ffmpeg",
"-i",
str(video_file),
"-vf",
f"subtitles={sub_name}",
"-c:v",
"libx264",
"-preset",
"medium",
"-crf",
"23",
"-c:a",
"copy",
"-y",
str(output_file),
]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0 and os.path.exists(output_file):
success = True
except Exception as e:
print(f"⚠️ subtitles 滤镜方法失败:{e}")
# 方法2: 如果方法1失败,尝试使用 shell 命令字符串
if not success:
print("\n⏳ 尝试备用方法...")
try:
cmd_str = (
f'ffmpeg -i "{video_file}" '
f'-vf "subtitles={sub_name}" '
f'-c:v libx264 -preset medium -crf 23 -c:a copy -y "{output_file}"'
)
result = subprocess.run(cmd_str, shell=True, capture_output=True, text=True)
if result.returncode == 0 and os.path.exists(output_file):
success = True
except Exception as e:
print(f"⚠️ 备用方法也失败:{e}")
# 清理临时文件
if temp_sub != subtitle_file and os.path.exists(temp_sub):
try:
os.remove(temp_sub)
except Exception as e:
print(f"⚠️ 清理临时文件失败:{e}")
if sub_name != os.path.basename(temp_sub) and os.path.exists(sub_name):
try:
os.remove(sub_name)
except Exception:
pass
if success:
file_size = os.path.getsize(output_file) / 1024 / 1024 # MB
# 获取输出视频信息
output_info = get_video_info(output_file)
duration = "未知"
if output_info:
duration = output_info["format"].get("duration", "未知")
print("\n" + "=" * 60)
print("✅ 字幕烧录成功!")
print("=" * 60)
print(f"📁 输出文件:{os.path.abspath(output_file)}")
print(f"⏱️ 视频时长:{duration}秒")
print(f"💾 文件大小:{file_size:.1f} MB")
print("=" * 60)
return True
else:
print("\n" + "=" * 60)
print("❌ 字幕烧录失败")
print("=" * 60)
print("✅ 字幕生成功能(generate-srt)已正常工作!")
print("📝 您已获得标准 SRT 格式的字幕文件:" + subtitle_file)
print("\n💡 如需要烧录字幕,可以使用以下方法:")
print(" 1. 使用专业视频编辑软件(如 Final Cut Pro、Premiere、剪映等)")
print(" 2. 使用在线视频工具(如 Kapwing、Clideo 等)")
print(" 3. 寻找已编译好 libass 支持的 ffmpeg 版本")
print("=" * 60)
return False
def main():
parser = argparse.ArgumentParser(
description="视频编辑器 - 合并多个视频、添加音频、合并音频、生成并烧录字幕",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
示例:
1. 合并多个视频:
python video_editor.py output.mp4 --videos video1.mp4 video2.mp4 video3.mp4
2. 合并视频并添加音频:
python video_editor.py output.mp4 --videos v1.mp4 v2.mp4 --audio narration.mp3
3. 为现有视频添加音频:
python video_editor.py output.mp4 --video input.mp4 --audio narration.mp3 --volume 0.8 --mode add-audio
4. 合并多个音频(按时间轴):
python video_editor.py output.mp3 --audios audio1.mp3,audio2.mp3 --start-times 0,5 --mode merge-audios
5. 生成 SRT 字幕文件:
python video_editor.py output.srt --subtitles '[{"text":"你好","start_time":0,"end_time":2},{"text":"世界","start_time":2.5,"end_time":5}]' --mode generate-srt
6. 烧录字幕到视频:
python video_editor.py output_with_subs.mp4 --video input.mp4 --subtitle subs.srt --mode burn-subtitles
7. 烧录字幕(自定义样式):
python video_editor.py output.mp4 --video input.mp4 --subtitle subs.srt --font-name "Arial" --font-size 28 --mode burn-subtitles
""",
)
parser.add_argument("output", help="输出文件路径")
parser.add_argument("--videos", nargs="+", help="要合并的视频文件列表")
parser.add_argument("--video", help="单个视频文件(用于添加音频或烧录字幕)")
parser.add_argument("--audio", help="单个音频文件(旁白、背景音乐等)")
parser.add_argument("--audios", help="要合并的音频文件列表,用逗号分隔")
parser.add_argument("--start-times", help="每个音频的开始时间(秒),用逗号分隔")
parser.add_argument(
"--volume", type=float, default=1.0, help="对白音频音量(0.0-2.0,默认 1.0)"
)
parser.add_argument(
"--bg-volume", type=float, default=1.0, help="背景音乐音量(0.0-2.0,默认 1.0)"
)
parser.add_argument(
"--subtitles",
help="字幕数据,JSON 格式的列表,每个元素包含 text、start_time、end_time",
)
parser.add_argument("--subtitle", help="SRT 字幕文件路径(用于烧录字幕)")
parser.add_argument("--font-name", default="Arial", help="字幕字体名称,默认 Arial")
parser.add_argument(
"--font-size", type=int, default=24, help="字幕字体大小,默认 24"
)
parser.add_argument(
"--font-color", default="white", help="字幕字体颜色,默认 white"
)
parser.add_argument(
"--outline-color", default="black", help="字幕边框颜色,默认 black"
)
parser.add_argument("--outline", type=int, default=2, help="字幕边框宽度,默认 2")
parser.add_argument(
"--shadow", type=int, default=0, help="字幕阴影深度,默认 0(无阴影)"
)
parser.add_argument(
"--alignment",
type=int,
default=2,
help="字幕对齐方式:1=左下, 2=中下, 3=右下, 8=左上, 9=中上, 10=右上,默认 2",
)
parser.add_argument(
"--mode",
choices=[
"merge",
"add-audio",
"merge-audios",
"generate-srt",
"burn-subtitles",
],
default="merge",
help="操作模式:merge(合并视频)、add-audio(添加音频)、merge-audios(合并音频)、generate-srt(生成 SRT 字幕)、burn-subtitles(烧录字幕)",
)
args = parser.parse_args()
# 只有非 generate-srt 模式才需要 ffmpeg
if args.mode != "generate-srt":
if not check_ffmpeg():
install_ffmpeg()
sys.exit(1)
# 确保输出目录存在
output_path = Path(args.output)
output_path.parent.mkdir(parents=True, exist_ok=True)
success = False
# 模式:合并视频
if args.mode == "merge":
if args.videos:
success = merge_videos(
args.videos, args.output, args.audio, args.volume, args.bg_volume
)
else:
print("❌ 请指定要合并的视频文件(使用 --videos 参数)")
print(
"示例:python video_editor.py output.mp4 --videos v1.mp4 v2.mp4 v3.mp4"
)
sys.exit(1)
# 模式:添加音频
elif args.mode == "add-audio":
if args.video and args.audio:
success = add_audio_to_video(
args.video, args.audio, args.output, args.volume
)
else:
print("❌ 请指定视频文件和音频文件")
print(
"示例:python video_editor.py output.mp4 --video input.mp4 --audio narration.mp3"
)
sys.exit(1)
# 模式:合并音频
elif args.mode == "merge-audios":
if args.audios and args.start_times:
audio_files = args.audios.split(",")
start_times = list(map(float, args.start_times.split(",")))
if len(audio_files) != len(start_times):
print("❌ 音频文件数量和开始时间数量不匹配")
sys.exit(1)
audio_files_with_timings = list(zip(audio_files, start_times))
success = merge_audios(audio_files_with_timings, args.output)
else:
print("❌ 请指定音频文件列表和开始时间列表")
print(
"示例:python video_editor.py output.mp3 --audios audio1.mp3,audio2.mp3 --start-times 0,5 --mode merge-audios"
)
sys.exit(1)
# 模式:生成 SRT 字幕
elif args.mode == "generate-srt":
if args.subtitles:
try:
subtitles = json.loads(args.subtitles)
success = generate_srt(subtitles, args.output)
except json.JSONDecodeError as e:
print(f"❌ 字幕数据 JSON 解析失败:{e}")
print(
'示例:--subtitles \'[{"text":"你好","start_time":0,"end_time":2}]\''
)
sys.exit(1)
else:
print("❌ 请指定字幕数据(使用 --subtitles 参数)")
print(
'示例:python video_editor.py output.srt --subtitles \'[{"text":"你好","start_time":0,"end_time":2}]\' --mode generate-srt'
)
sys.exit(1)
# 模式:烧录字幕
elif args.mode == "burn-subtitles":
if args.video and args.subtitle:
success = burn_subtitles_to_video(
args.video,
args.subtitle,
args.output,
args.font_name,
args.font_size,
args.font_color,
args.outline_color,
args.outline,
args.shadow,
args.alignment,
)
else:
print("❌ 请指定视频文件和字幕文件")
print(
"示例:python video_editor.py output.mp4 --video input.mp4 --subtitle subs.srt --mode burn-subtitles"
)
sys.exit(1)
sys.exit(0 if success else 1)
if __name__ == "__main__":
main()
Related skills
FAQ
What tool does it depend on?
It requires ffmpeg, installed via apt install ffmpeg on Ubuntu/Debian or brew install ffmpeg on macOS.
Can it add subtitles?
Yes, it generates SRT subtitle files and can burn them into video, though burning needs ffmpeg built with libass.