python+html+ffmpeg+openai兼容api实现ai自动视频剪辑软件bfwcutmaster代码

代码语言:python

所属分类:其他

版本2(最新):python+html+ffmpeg+openai兼容api实现ai自动视频剪辑软件bfwcutmaster代码

版本1(旧版):python+ffmpeg实现类似剪映的可视化剪辑软件代码bfwcutmaster

代码描述:python+html+ffmpeg+openai兼容api实现ai自动视频剪辑软件bfwcutmaster代码,增加了可视化元素缩放改变大小和拖拽移动位置,增加了接入openai的兼容api实现自动剪辑素材到时间线的功能。

代码标签: python html ffmpeg openai 兼容 api 实现 ai 自动 视频 剪辑 软件

下面为部分代码预览,完整代码请点击下载或在bfwstudio webide中打开

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Python + FFmpeg 可视化时间轴视频剪辑器(AI剪辑助手增强版)
运行: python video_editor.py
依赖: pip install flask ; 系统需安装 ffmpeg / ffprobe 并加入 PATH
"""

import os
import re
import json
import uuid
import base64
import shutil
import subprocess
import threading
import time
import urllib.request
import urllib.error
from datetime import datetime

from flask import Flask, request, jsonify, send_from_directory, Response

# ------------------------------------------------------------------
# 目录初始化
# ------------------------------------------------------------------
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
WORKSPACE = os.path.join(BASE_DIR, "workspace")
PROJECTS_DIR = os.path.join(WORKSPACE, "projects")
MEDIA_DIR = os.path.join(WORKSPACE, "media")
EXPORTS_DIR = os.path.join(WORKSPACE, "exports")
THUMBS_DIR = os.path.join(WORKSPACE, "thumbnails")
CONFIG_PATH = os.path.join(WORKSPACE, "config.json")

for d in (PROJECTS_DIR, MEDIA_DIR, EXPORTS_DIR, THUMBS_DIR):
    os.makedirs(d, exist_ok=True)

app = Flask(__name__)
EXPORT_STATUS = {}   # project_id -> {status, progress, output/message}

ALLOWED_VIDEO = {'.mp4', '.mov', '.avi', '.mkv', '.webm', '.flv', '.m4v'}
ALLOWED_AUDIO = {'.mp3', '.wav', '.aac', '.flac', '.m4a', '.ogg'}
ALLOWED_IMAGE = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp'}


# ==================================================================
# FFmpeg 工具函数
# ==================================================================
def run_cmd(cmd, timeout=None):
    return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=timeout)


def ffprobe_info(path):
    cmd = ["ffprobe", "-v", "quiet", "-print_format", "json",
           "-show_format", "-show_streams", path]
    try:
        out = subprocess.check_output(cmd, stderr=subprocess.STDOUT)
        data = json.loads(out.decode("utf-8", errors="ignore"))
    except Exception:
        return {"duration": 0, "width": 0, "height": 0, "has_audio": False, "has_video": False}

    duration = float(data.get("format", {}).get("duration", 0) or 0)
    width = height = 0
    has_audio = has_video = False
    for s in data.get("streams", []):
        if s.get("codec_type") == "video" and not has_video:
            width = int(s.get("width") or 0)
            height = int(s.get("height") or 0)
            has_video = True
            if not duration:
                duration = float(s.get("duration", 0) or 0)
        if s.get("codec_type") == "audio":
            has_audio = True
    return {"duration": duration, "width": width, "height": height,
            "has_audio": has_audio, "has_video": has_video}


def detect_media_type(filename):
    ext = os.path.splitext(filename)[1].lower()
    if ext in ALLOWED_VIDEO:
        return "video"
    if ext in ALLOWED_AUDIO:
        return "audio"
    if ext in ALLOWED_IMAGE:
        return "image"
    return "unknown"


def generate_thumbnail(src_path, out_path, media_type, duration=0):
    try:
        if media_type == "video":
            t = min(1.0, duration / 2) if duration else 0.3
            run_cmd(["ffmpeg", "-y", "-ss", str(t), "-i", src_path,
                      "-frames:v", "1", "-vf", "scale=160:-1", out_path], timeout=30)
        elif media_type == "image":
            run_cmd(["ffmpeg", "-y", "-i", src_path,
                      "-vf", "scale=160:-1", "-frames:v", "1", out_path], timeout=30)
    except Exception as e:
        print("生成缩略图失败:", e)


_CACHED_FONT = None
def find_system_font():
    """自动探测一个可用的中文字体文件,避免 Windows 下 drawtext 因缺少 fontconfig 崩溃"""
    global _CACHED_FONT
    if _CACHED_FONT is not None:
        return _CACHED_FONT
    candidates = []
    if os.name == "nt":
        win_fonts = os.path.join(os.environ.get("WINDIR", "C:/Windows"), "Fonts")
        candidates += [
            os.path.join(win_fonts, "msyh.ttc"),
            os.path.join(win_fonts, "msyhbd.ttc"),
            os.path.join(win_fonts, "simhei.ttf"),
            os.path.join(win_fonts, "simsun.ttc"),
            os.path.join(win_fonts, "arial.ttf"),
        ]
    elif os.sys.platform == "darwin":
        candidates += [
            "/System/Library/Fonts/PingFang.ttc",
            "/System/Library/Fonts/STHeiti Light.ttc",
            "/Library/Fonts/Arial Unicode.ttf",
        ]
    else:
        candidates += [
            "/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc",
            "/usr/share/fonts/truetype/wqy/wqy-microhei.ttc",
            "/usr/share/fonts/truetype/noto/NotoSansCJK-Regular.ttc",
            "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
            "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
        ]
    for c in candidates:
        if os.path.exists(c):
            _CACHED_FONT = c
            return c
    _CACHED_FONT = ""
    return ""


def escape_fontpath(path):
    path = path.replace("\\", "/")
    path = path.replace(":", "\\:")
    return path


# ==================================================================
# 项目管理
# ==================================================================
def project_path(pid):
    return os.path.join(PROJECTS_DIR, f"{pid}.json")


def load_project(pid):
    p = project_path(pid)
    if not os.path.exists(p):
        return None
    with open(p, "r", encoding="utf-8") as f:
        return json.load(f)


def save_project(pid, data):
    with open(project_path(pid), "w", encoding="utf-8") as f:
        json.dump(data, f, ensure_ascii=False, indent=2)


def list_projects():
    result = []
    for fn in os.listdir(PROJECTS_DIR):
        if fn.endswith(".json"):
            try:
                with open(os.path.join(PROJECTS_DIR, fn), "r", encoding="utf-8") as f:
                    d = json.load(f)
                result.append({
                    "id": d.get("id"),
                    "name": d.get("name"),
                    "created": d.get("created"),
                    "modified": d.get("modified"),
                    "resolution": d.get("resolution"),
                })
            except Exception:
                continue
    result.sort(key=lambda x: x.get("modified", ""), reverse=True)
    return result


def default_project(name):
    pid = uuid.uuid4().hex[:12]
    now = datetime.now().isoformat()
    return {
        "id": pid,
        "name": name,
        "created": now,
        "modified": now,
        "resolution": {"width": 1280, "height": 720},
        "fps": 30,
        "tracks": [
            {"id": uuid.uuid4().hex[:8], "type": "video", "name": "视频轨道1",
             "clips": [], "muted": False, "hidden": False},
            {"id": uuid.uuid4().hex[:8], "type": "audio", "name": "音频轨道1",
             "clips": [], "muted": False, "hidden": False},
            {"id": uuid.uuid4().hex[:8], "type": "subtitle", "name": "字幕轨道1",
             "clips": [], "muted": False, "hidden": False},
        ],
        "media": [],
        "aiChatHistory": []
    }


# ==================================================================
# 导出引擎(构建 ffmpeg filter_complex 并执行)
# ==================================================================
def escape_drawtext(text):
    text = text.replace("\\", "\\\\")
    text = text.replace(":", "\\:")
    text = text.replace("'", "\u2019")
    text = text.replace("\n", " ")
    return text


def build_atempo_chain(speed):
    filters = []
    s = speed if speed and speed > 0 else 1.0
    while s > 2.0:
        filters.append("atempo=2.0")
        s /= 2.0
    while s < 0.5:
        filters.append("atempo=0.5")
        s /= 0.5
    filters.append(f"atempo={s:.4f}")
    return filters


def compute_total_duration(project):
    end = 0.0
    for track in project["tracks"]:
        for clip in track["clips"]:
            e = clip.get("trackStart", 0) + clip.get("duration", 0)
            if e > end:
                end = e
    return max(end, 1.0)


def build_export(project, output_path):
    W = project["resolution"]["width"]
    H = project["resolution"]["height"]
    fps = project.get("fps", 30)
    total_dur = compute_total_duration(project)
    media_lookup = {m["id"]: m for m in project["media"]}

    input_args = []
    idx_counter = [0]

    def add_input(media, extra_pre=None):
        args = []
        if extra_pre:
            args += extra_pre
        args += ["-i", media["path"]]
        input_args.extend(args)
        i = idx_counter[0]
        idx_counter[0] += 1
        return i

    video_tracks = [t for t in project["tracks"] if t["type"] in ("video", "overlay", "sticker")]
    audio_tracks = [t for t in project["tracks"] if t["type"] == "audio"]
    subtitle_tracks = [t for t in project["tracks"] if t["type"] == "subtitle"]

    filter_lines = []
    filter_lines.append(f"color=c=black:s={W}x{H}:d={total_dur:.3f}:r={fps}[vbase0]")
    base_label = "vbase0"
    v_counter = 1
    audio_labels = []
    a_counter = 1

    # ---- 视频 / 叠加(画中画/贴纸) 轨道,按轨道顺序从下到上叠加 ----
    for track in video_tracks:
        if track.get("hidden"):
            continue
        for clip in track["clips"]:
            media = media_lookup.get(clip.get("mediaId"))
            if not media:
                continue
            speed = clip.get("speed", 1.0) or 1.0
            src_in = float(clip.get("sourceIn", 0))
            src_out = float(clip.get("sourceOut", media.get("duration", 5)))
            track_start = float(clip.get("trackStart", 0))
            duration = float(clip.get("duration", max(0.1, (src_out - src_in) / speed)))
            pos = clip.get("position") or {}
            x = pos.get("x", 0)
            y = pos.get("y", 0)
            w = int(pos.get("width", W))
            h = int(pos.get("height", H))
            opacity = float(clip.get("opacity", 1.0))

            if media["type"] == "image":
                iidx = add_input(media, extra_pre=["-loop", "1"])
                seg_len = duration * speed
                vf = (f"[{iidx}:v]trim=start=0:end={seg_len:.3f},setpts=PTS-STARTPTS,"
                      f"setpts=PTS/{speed}+{track_start:.3f}/TB,"
                      f"scale={w}:{h},format=rgba")
            else:
                iidx = add_input(media)
                vf = (f"[{iidx}:v]trim=start={src_in:.3f}:end={src_out:.3f},setpts=PTS-STARTPTS,"
                      f"setpts=PTS/{speed}+{track_start:.3f}/TB,"
                      f"scale={w}:{h}")

            label_v = f"v{v_counter}"
            if opacity < 0.999:
                vf += f",format=rgba,colorchannelmixer=aa={opacity:.3f}"
            vf += f"[{label_v}]"
            filter_lin.........完整代码请登录后点击上方下载按钮下载查看

网友评论0