作者: Jet L

  • 【Ai】在Windows系统本地部署DeepSeek-R1的极简步骤

    惊闻B站有人出售百元本地部署教程,NND给我看笑了,什么都能卖钱是吧,当然不排除人家手把手教,提供足够的情绪价值。

    但是如果你不想花钱,同时想提升一下英文水平和计算机熟练度,请跟着官方文档一步步进行,目前的文档已经相当详细且可行。

    我们如果在Windows上进行部署和调试,推荐使用Open WebUi+Ollama的方式进行部署。

    PS:其实更建议在Linux上进行部署,一键部署更加便利~

    1、选择后端Ollama

    在Ollama官网选择Win版本下载,会自动部署相关环境,在CMD或者中powershell可以按照对应的模型拉取到本地。

    请根据自己的硬件量力而行。我的显卡为RTX3070 8G,按照ollama默认设置,运行7B已经亚历山大。

    2、选择前端Open-WebUi

    请根据您的系统,在Open-WebUi的官方文档,按照步骤,一步步来进行部署。

    Win版本下按照官方建议,可以使用uv,在powershell中进行拉取和部署,其中对网络环境有一定要求。

    3、启动Open-WebUi后的注意事项

    Open-WebUi原版毕竟是国外软件,在国内这个环境启动还是有一点网络困扰的,尤其是有些文件是通过Github的地址获取的,请对自己的网络环境做出一些针对性的优化调整。

    另外完成本地部署后,如果是启动在127.0.0.1上,那么检查ollama的端口是否running,一般按照官方部署都可以进行顺畅进行(只有国内这个网不太顺畅)

    此外Open-WebUi默认检查OpenAi的API,这个选项可以在首次登陆后去管理员面板关闭,这样不会每次启动都遇到拉取模型缓慢、超时的情况。

    4、其他分享

    我的电脑采用13700K 32G DDR5 RTX3070 8G,但大模型运行需要大显存,8G显存只能算起步,参数量只能流畅运行7B左右的模型,因此一般的家用和办公电脑跑大模型都存在很大的限制。

    RTX3070 8G跑DeepSeek-R1:7B的速度——显存爆满,ollama默认设置,显示CPU处理占用10%,GPU处理占用90%,常规问答的response token/s在39左右 ,prompt_token/s在2500左右。但是大模型性能存在一定的短板,长上下文效果不尽如人意。

    RTX3070 8G跑DeepSeek-R1:14B,ollama默认设置命令行中速度可以接受,在WebUi中短回答response token/s约为14,较长的上下文降低到7不到,一半跑在GPU一半跑在CPU,长文本效果很差。

    RTX3070 8G跑DeepSeek-R1:32B,ollama默认设置在命令行中速度还行,缓慢但可以简单对话,处理长文本速度基本不可用。若用13700K跑在CPU则32G内容跑满,速度也是非常慢。

    在跑完Ai测试后,请关闭Ollama的进程,否则你将面临满占用的显存或内存~

    5、体验

    搭配Open-WebUi可以实现本地部署,多端使用,但是对体验影响更大的限制——模型本身——我们个人、及小公司的计算性能均没法有效支撑,本来大模型就是为了提升效率,本地部署一个跑的死慢的模型,对效率的提升实在是存疑。

    当然我鼓励大家都去本地部署体验一下,从中也可以获得一些乐趣,但是如果到实际应用层面,大一些的模型硬件需求激增,小公司玩这个自建后端的硬件成本还是太高了。

    因此,对小公司而言,可能选择一个大树,使用API,保护好自己的数据(真的是有价值的数据吗?)进行针对性的训练,拓展自己的RAG系统,做好本地化的情况下拥抱云计算,才是提升小公司效率的一条路吧。

    但话说回来,小公司真的愿意为这个人工和软件成本付费吗?

  • 【Python】提取视频画面并生成PPT

    比较笨的方法,用来提取PPT课程视频画面,并生成对应的PPT,代码检测黑屏但没有检测白屏,没有检测重复画面(因为有些人讲课会来回翻PPT),因此还有优化空间。内存占用会逐渐增多,不过测试没有出现崩溃的情况。

    PS:做完发现可以直接问讲课人要PPT原件,我,,,

    import cv2
    import os
    import numpy as np
    from pptx import Presentation
    from pptx.util import Inches
    from skimage.metrics import structural_similarity as ssim
    import tkinter as tk
    from tkinter import filedialog, messagebox
    
    # 选择视频和输出目录
    def select_video_and_output():
        video_path = filedialog.askopenfilename(title="选择视频文件", filetypes=[("MP4 files", "*.mp4")])
        if not video_path:
            messagebox.showwarning("选择视频", "未选择视频文件")
            return None, None
        
        output_dir = filedialog.askdirectory(title="选择输出目录")
        if not output_dir:
            messagebox.showwarning("选择输出目录", "未选择输出目录")
            return None, None
    
        pptx_path = os.path.join(output_dir, "output_presentation.pptx")
        return video_path, pptx_path
    
    # 处理视频并生成 PPT
    def process_video_to_ppt(video_path, pptx_path):
        os.makedirs("ppt_images", exist_ok=True)
        
        cap = cv2.VideoCapture(video_path)
        _, prev_frame = cap.read()
        prev_gray = cv2.cvtColor(prev_frame, cv2.COLOR_BGR2GRAY)
    
        frame_count = 0
        slide_count = 0
        images = []
        similarity_threshold = 0.95  # 提高 SSIM 阈值,减少相似图片
        brightness_threshold = 10  # 黑屏检测(平均亮度 < 10 认为是黑屏)
    
        def process_frame(frame):
            """ 计算 SSIM 相似度,判断是否保存该帧 """
            nonlocal prev_gray, slide_count
            gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
            score = ssim(prev_gray, gray)
    
            # 计算平均亮度,过滤黑屏
            avg_brightness = np.mean(gray)
            if avg_brightness < brightness_threshold:
                return  # 跳过黑屏帧
    
            if score < similarity_threshold:  
                img_path = os.path.join("ppt_images", f"slide_{slide_count}.jpg")
    
                # 确保不同的幻灯片才保存
                if len(images) == 0 or images[-1] != img_path:  
                    cv2.imwrite(img_path, frame)
                    images.append(img_path)
                    slide_count += 1
                    prev_gray = gray  # 只在确认变化时更新参考帧
    
        while cap.isOpened():
            ret, frame = cap.read()
            if not ret:
                break
    
            # 仅每隔 15 帧处理一次
            if frame_count % 15 == 0:
                process_frame(frame)
    
            frame_count += 1
    
        cap.release()
        # cv2.destroyAllWindows()
    
        # 创建 PPT
        prs = Presentation()
        for img in images:
            slide = prs.slides.add_slide(prs.slide_layouts[5])  # 空白幻灯片
            left, top, width, height = Inches(0), Inches(0), Inches(10), Inches(7.5)
            slide.shapes.add_picture(img, left, top, width, height)
    
        prs.save(pptx_path)
        messagebox.showinfo("完成", f"PPTX 生成完成: {pptx_path}")
    
    # 主函数
    def main():
        root = tk.Tk()
        root.withdraw()  # 隐藏主窗口
        video_path, pptx_path = select_video_and_output()
        if video_path and pptx_path:
            process_video_to_ppt(video_path, pptx_path)
    
    if __name__ == "__main__":
        main()
    
  • 【网站】正确配置Nginx及DNS解析,实现全站裸域名

    网站之前一直是www和裸域名并行,现在有了多个域名,因此想让www直接重定向到裸域名,但是实施过程比我想得要复杂的多。

    一、DNS解析

    设定www域名的CNAME解析,绑定到裸域名。

    二、Nginx配置重定向逻辑

    可以分成几个代码块,此外还需要注意证书的配置,示例:

    1、重定向 HTTP 到 HTTPS,并强制跳转到裸域名:

    server {
    listen 80;
    listen [::]:80;
    server_name example.cn www.example.cn;
    return 301 https://example.cn$request_uri;

    }

    2、重定向 HTTPS 的www 到裸域名

    server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name www.example.cn;
    return 301 https://example.cn$request_uri;

    }

    3、HTTPS 裸域名配置

    server {
    listen 443 ssl;
    listen [::]:443 ssl;
    server_name example.cn;

    }

    此外,还需要注意的是,Wordpress和Apache2搭配,可以很好的进行重写操作,比如链接结构的更改,但是变更到Nginx之后这种变更就没有那么的自动化了,这个问题在我从Apache2切换到Nginx之后去实现全站配置裸域名时出现得非常频繁。

    如果不想使用插件,可以在配置主站点的各种设置到裸域名后,直接去数据库对post表里的www域名进行查询和替换,实现快速的替换。

    UPDATE wp_posts
    SET post_content = REPLACE(post_content, ‘http://www.example.com’, ‘http://example.com’);

  • 【网站】通过模板函数及WP设置文件配置SMTP邮件服务

    为什么我们需要配置邮件服务?

    第一,配置邮件服务可以避免WP中勾选邮件通知但是后台未配置导致的评论发送延迟。第二,可以让找回密码等服务可用。(也就是你要是没有配置服务所谓的密码找回是无效的,当然有自己服务器的就无所谓了,有更加快捷的方法重置密码)

    服务器注意事项

    前提1:服务器端需要开放465端口。

    前提2:防火墙开放465端口。

    采用465端口是因为云服务器的提供商普遍禁25端口,尝试解封了一下没有什么卵用。

    因此我们可以使用465端口进行邮件服务,国内可以直接使用QQ邮箱,在设置中获取授权码、服务器地址等配置基础信息。Hotmail需要配置另外一套验证机制,有点麻烦了,还是建议使用QQ邮箱,响应速度很快。

    配置步骤

    一般分为两个步骤,对wp-config文件和function模板函数进行修改,其实也可以直接配置模板函数,不过理论上通过WP配置文件配置更加安全,因为权限更高。

    WP-Config:

    // SMTP 配置
    define('HOST', 'smtp.qq.com');// smtp服务器
    define('PORT', 465);// 端口
    define('USERNAME', 'email@qq.com'); // 设置邮箱
    define('PASSWORD', 'password'); // 授权码
    define('SECURE', 'ssl'); // ssl
    define('FROM', 'your_qq_email@qq.com'); // 邮箱
    define('FROM_NAME', 'Name'); // 发件人名称

    模板函数:

    function custom_phpmailer_settings($phpmailer) {
    $phpmailer->isSMTP();
    $phpmailer->Host = HOST;
    $phpmailer->SMTPAuth = true;
    $phpmailer->Port =PORT;
    $phpmailer->Username = USERNAME;
    $phpmailer->Password = PASSWORD;
    $phpmailer->SMTPSecure = SECURE;
    $phpmailer->From = FROM;
    $phpmailer->FromName = FROM_NAME;
    }
    add_action('phpmailer_init', 'custom_phpmailer_settings');
  • 【小贴士】从Apache2切换到Nginx的注意事项

    这两天尝试了一下Apache2,感觉还是Nginx的配置更加直观,因此又切换回了Nginx,不过之前删掉了Nginx,俺寻思我Nginx配置那么多次了,重安装还不是轻轻松松,没想到还是遇到了一系列问题。

    1、Nginx及PHP

    重新安装的Nginx默认用户是Nginx,使用PHP-FPM时默认用户要切换成www-data。

    确认PHP端口保持一致。

    2、Cerbot从Apache2切换到Nginx

    删除Apache2的话,Cerbot的配置文件会无法执行操作,我们在重新配置Cerbot时候只需要把其配置文件改为:

    authenticator = nginx
    installer = nginx

    如上即可正确配置证书更新设置。

    3、Fail2Ban之类的conf文件更新

    需要按照Nginx的日志地址进行更新。

    4、读取错误日志进行针对性优化

    这是个很好的习惯!

    总之还是得多上手配置几次,才能知道知识掌握的是否牢固。

  • 【小贴士】WordPress多站点配置错误的解决

    最近配置Wordpress的多站点,初次成功,后续改变了主站点的域名到裸域名,再次配置多站点时候出现五花八门的错误,什么重定向过多,什么连接不上数据库,如果遇到这种情况,那么大概率在重启多站点的时候,你会看到警告:存在已有的多站点数据。

    那么这时候请按照Wordpress的官方多站点配置进行设置和检查,可以很快定位到问题所在。

    如果是使用Apache2进行的网站配置,检查Rewrite模块及模块中的 ‘AllowOverride all’设置。

    重点检查的WP配置文件:wp-config.php

    需要重点检查的数据表:

    wp_blogs

    wp_blogmeta

    wp_blog_versions

    wp_registration_log

    wp_signups

    wp_site

    wp_sitemeta

    其实主要的原因是Wordpress在关闭多站点设置的时候,不会自动对数据库的多站点数据进行清理,所以在重配置的时候会看到警告,也可能会出现花式错误。

    请参考官方链接:

    WP多站点网络创建

    WP多站点网络管理

    WP多站点网络的Debug

    在成功配置多站点后,一定要注意检查网站的固定链接结构!否则在网页中通过固定连接引用的页面会失效。

    如果多站点配置完成出现Cookie的相关错误,或者点击登录、操作需要等待响应等现象,可以配置wp-config.php添加:

    define(‘COOKIE_DOMAIN’, $_SERVER[‘HTTP_HOST’]);
    define(‘COOKIEPATH’, ‘/’);

    刷新站点缓存及本地缓存,重置Cookie后再尝试。

  • 【小贴士】WordPress中锚点注意事项

    开始之前,建议先阅读:WordPress官方关于锚点的介绍

    WordPress在现在的古登堡编辑器中,可以在“侧边栏-高级-HTML锚点”中很方便为页面添加锚点,但是这其中有一些建议和注意事项。

    问题1、锚点的地址设置

    在WP目前的设置中,如果我们在页面中将锚点前加上完整的URL,点击锚点时,(在某些浏览器中)页面跳转会非常突兀,Not elegant。

    示例:

    因此我们在同一页面中只要直接设置“#锚点”,就可以实现顺滑的跳转。

    问题2、锚点的唯一性

    这是接着问题1引申出来的,我们在每个单独页面中肯定是会注意到锚点的唯一性,但是多页面的锚点一旦多起来呢?重复的话会发生什么问题?

    在一般的用户场景这个问题并不显著,但是在WP的合集页面(目录、标签)中,如果我们有多个关于MV的文章,里面每个MV都在各自页面设置了“#MV”的锚点,那么WP在合计页面只能选择第一个文章的“#MV”锚点进行跳转。

    因此,我们可以在结合WP文章的URL结构,在每个锚点单词前加上对应文章URL的数字码,可以一定程度确保每个锚点的唯一性。


    演示区域

    这是一个锚点

  • 【网站】通过nginx为站点启用HTTP2

    启用过程还是比较简单的,但是过程让我学习到,问Ai之前先看官方手册,,,毕竟Ai的知识库不一定是最新的,而且会一本正经的胡说八道!

    过程:

    1、通过nginx -v检查nginx版本,翻阅nginx的官网手册对nginx配置文件进行编辑。

    2、配置文件server部分要注意是:

    server {
    listen 443 ssl;
    http2 on;
    }

    有些教程会告诉你是listen 443 ssl http2;这一语法已经在1.25.1版本之后被弃用了!所以如果你的nginx版本较高,参考官网的语法进行启用!

    3、按照你的需求配置其他选项。

    4、最重要的,编辑完毕使用nginx -t指令测试nginx的配置文件是否通过,然后重启nginx服务即可。

    效果:

    访问网站,检查协议是否为h2。

  • 【Python】使用cwebp、gif2webp、exiftool实现保留exif信息的WebP转换

    此前写了个使用cwebp、gif2webp的脚本,但是由于cwebp目前在win的元数据提取存在问题,因此我们可以使用已经支持exif提取和写入的exiftool进行最后一步的转换,这样我们的图片压缩、转码都在官方库得以实现。

    前置条件:

    cwebp、gif2webp、exiftool三个组件都注册到系统环境变量。python则使用pil库用于分辨率获取。

    实现效果:

    使用pil库对分辨率进行获取,但是不介入压缩过程,因为cwebp目前没法获取图片分辨率,使用pil库进行是否执行resize的判断。

    使用cwebp处理静态png、jpg,使用gif2webp处理gif图,启用mt多线程,压缩质量85,resize到2560最长、宽边,exiftool采用”-overwrite_original”来避免生成两个图片。

    测试效果:

    该图片原图7M多,压缩质量选择85,可能由于细节较为丰富,压缩到WebP大小仍为1M左右,还是比较大,不过细节保留充分,同时保留了EXIF信息。

    import tkinter as tk
    from tkinter import filedialog, messagebox
    import os
    import subprocess
    from PIL import Image
    
    def validate_file(input_path):
        input_path = os.path.abspath(input_path)
        if not os.path.exists(input_path):
            raise FileNotFoundError(f"文件 {input_path} 不存在,请检查路径。")
        return input_path
    
    def get_resized_dimensions(width, height, max_size):
        if width > height:
            new_width = max_size
            new_height = int((new_width / width) * height)
        else:
            new_height = max_size
            new_width = int((new_height / height) * width)
        return new_width, new_height
    
    def convert_image(input_path, output_path, new_width=None, new_height=None):
        try:
            file_extension = os.path.splitext(input_path)[1].lower()
            if file_extension == ".gif":
                command = ["gif2webp","mt", input_path, "-o", output_path]
            else:
                if new_width and new_height:
                    command = ["cwebp","mt", "-q", "85", "-resize", str(new_width), str(new_height), input_path, "-o", output_path]
                else:
                    command = ["cwebp","mt", "-q", "85", input_path, "-o", output_path]
            subprocess.run(command, check=True)
        except subprocess.CalledProcessError as e:
            raise RuntimeError(f"转换工具运行出错: {e}")
    
    def embed_exif(input_path, output_path):
        try:
            command = ["exiftool", "-overwrite_original", "-tagsfromfile", input_path, "-all:all", output_path]
            subprocess.run(command, check=True)
        except subprocess.CalledProcessError as e:
            raise RuntimeError(f"EXIF 数据嵌入失败: {e}")
    
    def convert_to_webp(input_path, max_size=2560):
        try:
            # 验证文件路径
            input_path = validate_file(input_path)
            output_path = os.path.splitext(input_path)[0] + ".webp"
    
            # 使用 PIL 获取图像分辨率
            with Image.open(input_path) as img:
                width, height = img.size
                if width <= max_size and height <= max_size:
                    convert_image(input_path, output_path)
                else:
                    new_width, new_height = get_resized_dimensions(width, height, max_size)
                    convert_image(input_path, output_path, new_width, new_height)
    
            # 嵌入 EXIF 数据
            embed_exif(input_path, output_path)
    
            return f"图片已转换并保存为 {output_path}"
    
        except (subprocess.CalledProcessError, FileNotFoundError) as e:
            return str(e)
        except Exception as e:
            return f"处理文件时发生错误: {e}"
    
    def select_files():
        file_paths = filedialog.askopenfilenames(
            title="选择图片文件",
            filetypes=[("*所有图片格式", "*.jpg;*.jpeg;*.png;*.gif"),
                       ("JPEG 图片", "*.jpg;*.jpeg"),
                       ("PNG 图片", "*.png"),
                       ("GIF 图片", "*.gif")]
        )
        if file_paths:
            for path in file_paths:
                file_listbox.insert(tk.END, path)
    
    def convert_and_save_batch():
        files = file_listbox.get(0, tk.END)
        if not files:
            messagebox.showerror("错误", "请选择至少一个图片文件!")
            return
    
        results = [convert_to_webp(file_path) for file_path in files]
        messagebox.showinfo("完成", "\n".join(results))
    
    def clear_list():
        file_listbox.delete(0, tk.END)
    
    root = tk.Tk()
    root.title("批量图片转换为 WebP 工具")
    root.geometry("600x400")
    
    frame = tk.Frame(root)
    frame.pack(pady=10, padx=10, fill=tk.BOTH, expand=True)
    
    scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL)
    file_listbox = tk.Listbox(frame, selectmode=tk.EXTENDED, yscrollcommand=scrollbar.set)
    scrollbar.config(command=file_listbox.yview)
    scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
    file_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
    
    button_frame = tk.Frame(root)
    button_frame.pack(pady=10)
    
    select_button = tk.Button(button_frame, text="选择文件", command=select_files, width=15)
    select_button.grid(row=0, column=0, padx=5)
    
    clear_button = tk.Button(button_frame, text="清空列表", command=clear_list, width=15)
    clear_button.grid(row=0, column=1, padx=5)
    
    convert_button = tk.Button(button_frame, text="批量转换", command=convert_and_save_batch, width=15)
    convert_button.grid(row=0, column=2, padx=5)
    
    root.mainloop()
    
  • 【Python】使用WebP官方库进行WebP转换

    此前的代码使用了Pillow库集成的库,这次使用WebP官方库,对GIF、PNG的处理也比较友好。需要添加WebP的库到系统环境变量后使用。

    功能实现:

    代码使用cwebp、gif2webp两种方式转换不同的格式图片,使用库本身的压缩分辨率方法,压缩图片到2560最长、宽。

    对于gif,该代码可以实现对原图动态的保持,此前使用pillow库则可以设定gif的持续或者最后一帧静帧。

    遗憾:

    没有能够传递exif,win平台中cwebp压根没法有效传递exif信息,显示——Warning: only ICC profile extraction is currently supported on this platform!元数据只有ICC才能支持传递,所以只依靠cwebp是没法很好在win中进行转换的。

    这个问题采用exiftool进行了解决,下篇文章可以看到。

    import tkinter as tk
    from tkinter import filedialog, messagebox
    import os
    import subprocess
    from PIL import Image
    
    def validate_file(input_path):
        input_path = os.path.abspath(input_path)
        if not os.path.exists(input_path):
            raise FileNotFoundError(f"文件 {input_path} 不存在,请检查路径。")
        return input_path
    
    def get_resized_dimensions(width, height, max_size):
        if width > height:
            new_width = max_size
            new_height = int((new_width / width) * height)
        else:
            new_height = max_size
            new_width = int((new_height / height) * width)
        return new_width, new_height
    
    # 使用 cwebp 或 gif2webp 进行转换
    def convert_image(input_path, output_path, new_width=None, new_height=None, cwebp_metadata="none", gif2webp_metadata="none"):
        try:
            file_extension = os.path.splitext(input_path)[1].lower()
            if file_extension == ".gif":
                command = ["gif2webp", "-metadata", gif2webp_metadata, "-mt", input_path, "-o", output_path]
            else:
                if new_width and new_height:
                    command = ["cwebp", "-q", "85", "-resize", str(new_width), str(new_height), "-metadata", cwebp_metadata, "-mt", input_path, "-o", output_path]
                else:
                    command = ["cwebp", "-q", "85", "-metadata", cwebp_metadata, "-mt", input_path, "-o", output_path]
            subprocess.run(command, check=True)
        except subprocess.CalledProcessError as e:
            raise RuntimeError(f"转换工具运行出错: {e}")
    
    def convert_to_webp(input_path, max_size=2560, cwebp_metadata="none", gif2webp_metadata="none"):
        try:
            # 验证文件路径
            input_path = validate_file(input_path)
            output_path = os.path.splitext(input_path)[0] + ".webp"
    
            # 使用 PIL 获取图像分辨率
            with Image.open(input_path) as img:
                width, height = img.size
                if width <= max_size and height <= max_size:
                    convert_image(input_path, output_path, cwebp_metadata=cwebp_metadata, gif2webp_metadata=gif2webp_metadata)
                else:
                    new_width, new_height = get_resized_dimensions(width, height, max_size)
                    convert_image(input_path, output_path, new_width, new_height, cwebp_metadata=cwebp_metadata, gif2webp_metadata=gif2webp_metadata)
    
            return f"图片已转换并保存为 {output_path}"
    
        except (subprocess.CalledProcessError, FileNotFoundError) as e:
            return str(e)
        except Exception as e:
            return f"处理文件时发生错误: {e}"
    
    def select_files():
        file_paths = filedialog.askopenfilenames(
            title="选择图片文件",
            filetypes=[("*所有图片格式", "*.jpg;*.jpeg;*.png;*.gif"),
                       ("JPEG 图片", "*.jpg;*.jpeg"),
                       ("PNG 图片", "*.png"),
                       ("GIF 图片", "*.gif")]
        )
        if file_paths:
            for path in file_paths:
                file_listbox.insert(tk.END, path)
    
    def convert_and_save_batch():
        files = file_listbox.get(0, tk.END)
        if not files:
            messagebox.showerror("错误", "请选择至少一个图片文件!")
            return
    
        cwebp_metadata = "exif"  # 设置cwebp要复制的元数据类型为exif
        gif2webp_metadata = "xmp"  # 设置gif2webp要复制的元数据类型为xmp
        results = [convert_to_webp(file_path, cwebp_metadata=cwebp_metadata, gif2webp_metadata=gif2webp_metadata) for file_path in files]
        messagebox.showinfo("完成", "\n".join(results))
    
    def clear_list():
        file_listbox.delete(0, tk.END)
    
    # 创建主窗口
    root = tk.Tk()
    root.title("批量图片转换为 WebP 工具")
    root.geometry("600x400")
    
    frame = tk.Frame(root)
    frame.pack(pady=10, padx=10, fill=tk.BOTH, expand=True)
    
    scrollbar = tk.Scrollbar(frame, orient=tk.VERTICAL)
    file_listbox = tk.Listbox(frame, selectmode=tk.EXTENDED, yscrollcommand=scrollbar.set)
    scrollbar.config(command=file_listbox.yview)
    scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
    file_listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
    
    button_frame = tk.Frame(root)
    button_frame.pack(pady=10)
    
    select_button = tk.Button(button_frame, text="选择文件", command=select_files, width=15)
    select_button.grid(row=0, column=0, padx=5)
    
    clear_button = tk.Button(button_frame, text="清空列表", command=clear_list, width=15)
    clear_button.grid(row=0, column=1, padx=5)
    
    convert_button = tk.Button(button_frame, text="批量转换", command=convert_and_save_batch, width=15)
    convert_button.grid(row=0, column=2, padx=5)
    
    root.mainloop()