在你的 Mac Mx 上运行 Flux.1 Dev/Schnell + Lora,而不使用 ComfyUI


7 个月前

这篇文章的目的是提出一种在 Mac 上运行 Flux.1 的替代方法,目前大多数可用的教程都是基于 ComfyUI

我尝试找到一种简单的方法来生成图像,使用 stable-diffusion.cpp,它现在支持 Flux.1 模型。

你将找到一个简单的 Python 应用程序,用于驱动 stable-diffusion.cpp,以支持 Flux.1 Dev 和 Flux.1 Schnell 以及大多数兼容的 Lora 适配器。

前提条件

你首先需要在 hugginface 创建一个账户并登录,然后生成一个令牌以获取模型和 Lora 适配器:

  1. 进入你的用户账户,设置,并创建一个令牌:

None

在这里创建令牌

  1. 在你的 Mac 上安装 hugginface 命令行工具
hugginface-cli login

然后输入你的令牌。

  1. 创建一个文件夹并下载/构建 stable-diffusion.cpp
mkdir flux.1
cd flux.1
mkdir models

git clone --recursive https://github.com/leejet/stable-diffusion.cpp
cd stable-diffusion.cpp
mkdir build
cd build
cmake .. -DSD_METAL=ON
cmake --build . --config Release

现在你应该在 ./bin 文件夹中有一个新的二进制命令 "sd"。

  1. 下载模型

返回到你的 "flux.1" 文件夹,你应该看到其中的 "./models" 文件夹,然后使用以下命令下载所有模型:

cd ./models

huggingface-cli download --local-dir ./ leejet/FLUX.1-schnell-gguf flux1-schnell-q8_0.gguf
huggingface-cli download --local-dir ./ leejet/FLUX.1-dev-gguf flux1-dev-q8_0.gguf

## Vae 
huggingface-cli download --local-dir ./ black-forest-labs/FLUX.1-dev ae.safetensors

## clip_l
huggingface-cli download --local-dir ./ comfyanonymous/flux_text_encoders clip_l.safetensors

## t5xxl
huggingface-cli download --local-dir ./ comfyanonymous/flux_text_encoders t5xxl_fp16.safetensors

## Lora
huggingface-cli download --local-dir ./ XLabs-AI/flux-lora-collection anime_lora_comfy_converted.safetensors
huggingface-cli download --local-dir ./ XLabs-AI/flux-lora-collection art_lora_comfy_converted.safetensors
huggingface-cli download --local-dir ./ XLabs-AI/flux-lora-collection disney_lora_comfy_converted.safetensors
huggingface-cli download --local-dir ./ XLabs-AI/flux-lora-collection mjv6_lora_comfy_converted.safetensors
huggingface-cli download --local-dir ./ XLabs-AI/flux-lora-collection realism_lora_comfy_converted.safetensors
huggingface-cli download --local-dir ./ XLabs-AI/flux-lora-collection scenery_lora_comfy_converted.safetensors

图形用户界面

这个 Python GUI 是使用 Panel 库编写的;它允许生成 "sd" 命令行参数并显示结果。

每次生成图像时,命令行及其参数都会在终端中写出,你可以复制该行以进行批处理。

import panel as pn
import param
import subprocess
import os
from typing import List, Tuple
from panel.widgets import Spinner
from threading import Thread

class StableDiffusionApp(param.Parameterized):
    model = param.ObjectSelector(default="Flux1 Dev", objects=["Flux1 Dev", "Flux1 Schnell"])
    lora_adapters = param.ObjectSelector(default=None, objects=["None"])
    prompt = param.String(default="")
    negative_prompt = param.String(default="")
    resolution = param.ObjectSelector(default="512x512", objects=["512x512", "1024x1024"])
    steps = param.Integer(default=4, bounds=(1, 100))
    seed = param.Integer(default=-1)
    command_line = param.String(default="")
    image_pane = param.ClassSelector(class_=pn.pane.Image, default=pn.pane.Image(sizing_mode='scale_both', min_height=512))
    command_output = param.String(default="")

    def __init__(self, **params):
        super(StableDiffusionApp, self).__init__(**params)
        self.generate_button = pn.widgets.Button(name="生成图像", button_type="primary")
        self.generate_button.on_click(self.run_generation)
        self.loading_spinner = pn.indicators.LoadingSpinner(value=False, size=25)
        self.image_pane = pn.pane.Image(sizing_mode='scale_both', min_height=512)
        self.output_image = ""
        self.update_lora_adapters()

    def update_lora_adapters(self) -> None:
        """更新可用的 LoRA 适配器列表。"""
        if not os.path.exists("./models"):
            print("未找到模型目录。请创建它并添加 LoRA 适配器文件。")
            return
        lora_files = ["None"] + [f for f in os.listdir("./models") if "lora" in f.lower() and f.endswith(".safetensors")]
        self.param.lora_adapters.objects = lora_files
        if lora_files:
            self.lora_adapters = "None"
        else:
            print("在模型目录中未找到 LoRA 适配器文件。")

    def generate_command(self) -> str:
        """生成 stable-diffusion.cpp 命令行。"""
        width, height = map(int, self.resolution.split('x'))
        lora_name = self.lora_adapters.replace('.safetensors', '') if self.lora_adapters != "None" else "no_lora"
        self.output_image = f"{self.prompt.replace(' ', '_')}_{self.seed}_{lora_name}_{width}x{height}_{self.steps}steps.png"
        
        # 用 LoRA 信息更新提示
        effective_prompt = self.prompt
        if self.lora_adapters != "None":
            effective_prompt += f" <{lora_name}:1>"

        cmd = [
            "./stable-diffusion.cpp/build/bin/sd",
            f"--diffusion-model ./models/{self.model.replace(' ', '-').lower()}-q8_0.gguf",
            f"--prompt '{effective_prompt}'",
            f"--negative-prompt '{self.negative_prompt}'",
            f"-W {width}",
            f"-H {height}",
            f"--steps {self.steps}",
            f"--seed {self.seed}",
            "--vae ./models/ae.safetensors",
            "--clip_l ./models/clip_l.safetensors",
            "--t5xxl ./models/t5xxl_fp16.safetensors",
            "--lora-model-dir ./models",
            "--cfg-scale 1.0",
            "--sampling-method euler",
            f"-o {self.output_image}",
            "-v"
        ]

        return " ".join(cmd)

    @param.depends('model', 'lora_adapters', 'prompt', 'negative_prompt', 'resolution', 'steps', 'seed', watch=True)
    def update_command_line(self):
        """更新命令行预览。"""
        if not hasattr(self, '_updating_command_line'):
            self._updating_command_line = True
            try:
                self.command_line = self.generate_command()
            finally:
                del self._updating_command_line

    def run_generation(self, event):
        """运行 stable-diffusion.cpp 命令并更新输出图像。"""
        cmd = self.generate_command()
        self.command_output = ""
        
        # 禁用按钮并启动加载指示器
        self.generate_button.disabled = True
        self.loading_spinner.value = True
        
        def run_command():
            try:
                process = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
                while True:
                    output = process.stdout.readline()
                    if output == '' and process.poll() is not None:
                        break
                    if output:
                        self.command_output += output
                        self.param.trigger('command_output')
                
                stderr = process.stderr.read()
                if stderr:
                    print(f"错误: {stderr}")
                    self.error_message = f"错误: {stderr}"
                self.image_pane.object = self.output_image  # 设置新图像
                self.param.trigger('image_pane')
            except subprocess.CalledProcessError as e:
                self.output_image = ""
            finally:
                # 重新启用按钮并停止加载指示器
                self.generate_button.disabled = False
                self.loading_spinner.value = False
                pn.io.push_notebook()

        # 在单独的线程中运行命令
        Thread(target=run_command).start()

    @param.depends('model', 'lora_adapters', 'prompt', 'negative_prompt', 'resolution', 'steps', 'seed', 'image_pane', watch=True)
    def view(self):
        """创建应用程序的主视图。"""
        input_column = pn.Column(
            pn.pane.Markdown("# Flux.1 Cpp"),
            pn.Param(self.param, 
                     parameters=['model', 'lora_adapters', 'prompt', 'negative_prompt', 'resolution', 'steps', 'seed'],
                     widgets={
                        'model': pn.widgets.Select,
                        'lora_adapters': pn.widgets.Select,
                        'prompt': pn.widgets.TextAreaInput,
                        'negative_prompt': pn.widgets.TextAreaInput,
                        'resolution': pn.widgets.Select,
                        'steps': pn.widgets.IntSlider,
                        'seed': pn.widgets.IntInput,
                     },
                     show_name=False),

            pn.Row(self.generate_button, self.loading_spinner),
        )

        output_column = pn.Column(
            pn.pane.Markdown("## 生成的图像"),
            self.image_pane,
            pn.Row(
                pn.Column(
                    pn.pane.Markdown("## 命令预览"),
                    pn.widgets.TextAreaInput(value=self.param.command_line, disabled=True, height=100),
                ),
                pn.Column(
                    pn.pane.Markdown("## 命令输出"),
                    pn.widgets.TextAreaInput(value=self.param.command_output, disabled=True, height=100, width=400, max_length=10000),
                )
            )   
        )

        return pn.Row(input_column, output_column)

# 创建并显示应用程序
app = StableDiffusionApp()
pn.serve(app.view, port=5006, show=True)

#### 结论

我希望这个简短的教程能帮助到你们!

关于性能,每个步骤大约需要 5 秒钟,适用于 Flux.1 Dev 和 Lora。

图像将保存在 "flux.1" 文件夹中。

你可以在 [github](https://github.com/ezeeFlop/flux.1) 找到所有代码!

如果你觉得这篇文章有用,我会很感激你给我一些掌声!

非常感谢,祝你生成愉快。

FluxAI 中文

© 2025. All Rights Reserved