diff --git a/client/gui/dialogs/BlackholeResolveDialog.cpp b/client/gui/dialogs/BlackholeResolveDialog.cpp index 73e0dd4..83065cb 100644 --- a/client/gui/dialogs/BlackholeResolveDialog.cpp +++ b/client/gui/dialogs/BlackholeResolveDialog.cpp @@ -3,13 +3,18 @@ #include #include #include +#include #include #include +#include #include #include namespace { +const QString kDefaultFluxInpaintPrompt = QStringLiteral( + "这是一幅中国山水画的一部分,mask是被我去除的一个人物,现在需要在原来的位置填充与周围画布连贯的背景"); + QPushButton* makeAlgoButton(const QString& title, QWidget* parent) { auto* btn = new QPushButton(parent); btn->setCheckable(false); @@ -41,6 +46,13 @@ BlackholeResolveDialog::BlackholeResolveDialog(const QString& blackholeName, QWi m_pages->setCurrentWidget(m_pageSelect); } +QString BlackholeResolveDialog::inpaintModelKey() const { + if (m_radioFlux && m_radioFlux->isChecked()) { + return QStringLiteral("flux_fill"); + } + return QStringLiteral("lama"); +} + QString BlackholeResolveDialog::promptText() const { return m_promptEdit ? m_promptEdit->toPlainText().trimmed() : QString(); } @@ -104,7 +116,7 @@ void BlackholeResolveDialog::buildDetailPage() { auto* pLay = new QVBoxLayout(panel); pLay->setSpacing(8); - auto* tip = new QLabel(QStringLiteral("应用后在画布拖取样框。"), panel); + auto* tip = new QLabel(QStringLiteral("在画布拖动取样。"), panel); tip->setStyleSheet("color: palette(mid);"); pLay->addWidget(tip); cLay->addWidget(panel); @@ -116,13 +128,13 @@ void BlackholeResolveDialog::buildDetailPage() { { auto* oLay = new QVBoxLayout(m_originalDetail); oLay->setSpacing(8); - auto* desc = new QLabel(QStringLiteral("仅隐藏黑洞,不改动背景文件。"), m_originalDetail); + auto* desc = new QLabel(QStringLiteral("不修改背景原图。"), m_originalDetail); desc->setWordWrap(true); oLay->addWidget(desc); oLay->addStretch(1); } - // 详情页 C:模型补全(提示词) + // 详情页 C:模型补全 m_modelDetail = new QWidget(m_algoDetails); { auto* mLay = new QVBoxLayout(m_modelDetail); @@ -133,11 +145,49 @@ void BlackholeResolveDialog::buildDetailPage() { auto* pLay = new QVBoxLayout(panel); pLay->setSpacing(8); + m_radioLama = new QRadioButton(QStringLiteral("LaMa"), panel); + m_radioFlux = new QRadioButton(QStringLiteral("FLUX Fill"), panel); + m_radioLama->setChecked(true); + pLay->addWidget(m_radioLama); + pLay->addWidget(m_radioFlux); + + m_promptLabel = new QLabel(QStringLiteral("提示词"), panel); + pLay->addWidget(m_promptLabel); + m_promptEdit = new QPlainTextEdit(panel); - m_promptEdit->setPlaceholderText(QStringLiteral("可选")); m_promptEdit->setMinimumHeight(72); + m_promptLabel->setVisible(false); + m_promptEdit->setVisible(false); pLay->addWidget(m_promptEdit); + auto syncPromptUi = [this]() { + if (!m_promptEdit || !m_promptLabel) { + return; + } + const bool flux = m_radioFlux && m_radioFlux->isChecked(); + m_promptLabel->setVisible(flux); + m_promptEdit->setVisible(flux); + if (flux) { + m_promptEdit->setEnabled(true); + if (m_promptEdit->toPlainText().trimmed().isEmpty()) { + m_promptEdit->setPlainText(kDefaultFluxInpaintPrompt); + } + } else { + m_promptEdit->setEnabled(false); + } + }; + connect(m_radioLama, &QRadioButton::toggled, this, [syncPromptUi](bool on) { + if (on) { + syncPromptUi(); + } + }); + connect(m_radioFlux, &QRadioButton::toggled, this, [syncPromptUi](bool on) { + if (on) { + syncPromptUi(); + } + }); + syncPromptUi(); + mLay->addWidget(panel); mLay->addStretch(1); } @@ -154,7 +204,14 @@ void BlackholeResolveDialog::buildDetailPage() { connect(btnBack, &QPushButton::clicked, this, [this]() { m_pages->setCurrentWidget(m_pageSelect); }); - connect(btnApply, &QPushButton::clicked, this, &QDialog::accept); + connect(btnApply, &QPushButton::clicked, this, [this]() { + if (m_selectedAlgorithm == Algorithm::ModelInpaint && m_radioFlux && m_radioFlux->isChecked() && + promptText().isEmpty()) { + QMessageBox::warning(this, QStringLiteral("模型补全"), QStringLiteral("请填写提示词。")); + return; + } + accept(); + }); connect(btnCancel, &QPushButton::clicked, this, &QDialog::reject); layout->addWidget(btns); diff --git a/client/gui/dialogs/BlackholeResolveDialog.h b/client/gui/dialogs/BlackholeResolveDialog.h index 772c8d2..bc416dc 100644 --- a/client/gui/dialogs/BlackholeResolveDialog.h +++ b/client/gui/dialogs/BlackholeResolveDialog.h @@ -3,6 +3,8 @@ #include class QLabel; +class QPlainTextEdit; +class QRadioButton; class QStackedWidget; class BlackholeResolveDialog final : public QDialog { @@ -17,6 +19,8 @@ public: explicit BlackholeResolveDialog(const QString& blackholeName, QWidget* parent = nullptr); Algorithm selectedAlgorithm() const { return m_selectedAlgorithm; } + QString inpaintModelKey() const; + QString promptText() const; private: @@ -41,6 +45,9 @@ private: QWidget* m_originalDetail = nullptr; QWidget* m_modelDetail = nullptr; - class QPlainTextEdit* m_promptEdit = nullptr; + QRadioButton* m_radioLama = nullptr; + QRadioButton* m_radioFlux = nullptr; + QLabel* m_promptLabel = nullptr; + QPlainTextEdit* m_promptEdit = nullptr; }; diff --git a/client/gui/dialogs/InpaintPreviewDialog.cpp b/client/gui/dialogs/InpaintPreviewDialog.cpp index 2af2d3a..189bf24 100644 --- a/client/gui/dialogs/InpaintPreviewDialog.cpp +++ b/client/gui/dialogs/InpaintPreviewDialog.cpp @@ -1,11 +1,13 @@ #include "dialogs/InpaintPreviewDialog.h" #include -#include +#include #include #include -#include +#include +#include #include +#include #include static QLabel* makeImageLabel(QWidget* parent) { @@ -14,17 +16,10 @@ static QLabel* makeImageLabel(QWidget* parent) { lab->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Ignored); lab->setScaledContents(false); lab->setAlignment(Qt::AlignCenter); + lab->setMinimumSize(1, 1); return lab; } -static QScrollArea* wrapScroll(QWidget* child, QWidget* parent) { - auto* sc = new QScrollArea(parent); - sc->setWidget(child); - sc->setWidgetResizable(false); - sc->setBackgroundRole(QPalette::Dark); - return sc; -} - InpaintPreviewDialog::InpaintPreviewDialog(const QString& title, QWidget* parent) : QDialog(parent) { setModal(true); @@ -37,14 +32,12 @@ InpaintPreviewDialog::InpaintPreviewDialog(const QString& title, QWidget* parent m_beforeLabel = makeImageLabel(this); m_afterLabel = makeImageLabel(this); - m_beforeScroll = wrapScroll(m_beforeLabel, this); - m_afterScroll = wrapScroll(m_afterLabel, this); - m_beforeScroll->setMinimumHeight(200); - m_afterScroll->setMinimumHeight(200); + m_beforeLabel->installEventFilter(this); + m_afterLabel->installEventFilter(this); auto* splitter = new QSplitter(Qt::Horizontal, this); - splitter->addWidget(m_beforeScroll); - splitter->addWidget(m_afterScroll); + splitter->addWidget(m_beforeLabel); + splitter->addWidget(m_afterLabel); splitter->setStretchFactor(0, 1); splitter->setStretchFactor(1, 1); root->addWidget(splitter, 1); @@ -58,17 +51,51 @@ InpaintPreviewDialog::InpaintPreviewDialog(const QString& title, QWidget* parent } void InpaintPreviewDialog::setImages(const QImage& before, const QImage& after) { - if (m_beforeLabel) { - const QPixmap pb = QPixmap::fromImage(before); - m_beforeLabel->setPixmap(pb); - m_beforeLabel->setMinimumSize(pb.size()); - m_beforeLabel->resize(pb.size()); - } - if (m_afterLabel) { - const QPixmap pa = QPixmap::fromImage(after); - m_afterLabel->setPixmap(pa); - m_afterLabel->setMinimumSize(pa.size()); - m_afterLabel->resize(pa.size()); - } + m_beforeImg = before; + m_afterImg = after; + updateScaledPreviews(); + QTimer::singleShot(0, this, [this]() { updateScaledPreviews(); }); } +void InpaintPreviewDialog::resizeEvent(QResizeEvent* e) { + QDialog::resizeEvent(e); + updateScaledPreviews(); +} + +void InpaintPreviewDialog::showEvent(QShowEvent* e) { + QDialog::showEvent(e); + updateScaledPreviews(); +} + +bool InpaintPreviewDialog::eventFilter(QObject* watched, QEvent* event) { + if (!m_inUpdateScaledPreviews && event->type() == QEvent::Resize) { + if (watched == m_beforeLabel || watched == m_afterLabel) { + updateScaledPreviews(); + } + } + return QDialog::eventFilter(watched, event); +} + +void InpaintPreviewDialog::updateScaledPreviews() { + if (m_inUpdateScaledPreviews) { + return; + } + m_inUpdateScaledPreviews = true; + auto fit = [](QLabel* lab, const QImage& src) { + if (!lab || src.isNull()) { + if (lab) { + lab->clear(); + } + return; + } + const QSize cap = lab->size(); + if (cap.width() < 2 || cap.height() < 2) { + return; + } + const QImage scaled = src.scaled(cap, Qt::KeepAspectRatio, Qt::SmoothTransformation); + lab->setPixmap(QPixmap::fromImage(scaled)); + }; + fit(m_beforeLabel, m_beforeImg); + fit(m_afterLabel, m_afterImg); + m_inUpdateScaledPreviews = false; +} diff --git a/client/gui/dialogs/InpaintPreviewDialog.h b/client/gui/dialogs/InpaintPreviewDialog.h index 9379252..6bd3c73 100644 --- a/client/gui/dialogs/InpaintPreviewDialog.h +++ b/client/gui/dialogs/InpaintPreviewDialog.h @@ -3,8 +3,10 @@ #include #include +class QEvent; class QLabel; -class QScrollArea; +class QShowEvent; +class QResizeEvent; class InpaintPreviewDialog final : public QDialog { Q_OBJECT @@ -13,10 +15,17 @@ public: void setImages(const QImage& before, const QImage& after); +protected: + void resizeEvent(QResizeEvent* e) override; + void showEvent(QShowEvent* e) override; + bool eventFilter(QObject* watched, QEvent* event) override; + private: + void updateScaledPreviews(); + QLabel* m_beforeLabel = nullptr; QLabel* m_afterLabel = nullptr; - QScrollArea* m_beforeScroll = nullptr; - QScrollArea* m_afterScroll = nullptr; + QImage m_beforeImg; + QImage m_afterImg; + bool m_inUpdateScaledPreviews = false; }; - diff --git a/client/gui/main_window/MainWindow.cpp b/client/gui/main_window/MainWindow.cpp index a906f7a..b4a5318 100644 --- a/client/gui/main_window/MainWindow.cpp +++ b/client/gui/main_window/MainWindow.cpp @@ -5006,11 +5006,12 @@ void MainWindow::showBlackholeContextMenu(const QPoint& globalPos, const QString client->setBaseUrl(QUrl(base)); QString immediateErr; + const QString inpaintModel = dlg.inpaintModelKey(); const QString prompt = dlg.promptText(); QNetworkReply* reply = client->inpaintAsync( cropPng, maskPng, - QStringLiteral("flux_fill"), + inpaintModel, prompt, QString(), 0.72, diff --git a/python_server/model/Inpaint/flux_fill/loader.py b/python_server/model/Inpaint/flux_fill/loader.py index ddf41b8..e38354f 100644 --- a/python_server/model/Inpaint/flux_fill/loader.py +++ b/python_server/model/Inpaint/flux_fill/loader.py @@ -7,6 +7,32 @@ import numpy as np from PIL import Image +def _align_spatial_dim_for_flux_fill(n: int, multiple: int = 16) -> int: + """FLUX Fill 要求 H/W 为 (vae_scale_factor*2) 的倍数,常见为 16。""" + n = int(max(n, multiple)) + return n - (n % multiple) + + +def _flux_clip_prompt_and_t5_full(pipe: Any, text_full: str) -> tuple[str, str]: + """ + CLIP 仅约 77 token:用与管线一致的 tokenizer 截断得到 prompt(池化向量)。 + 完整文本走 prompt_2 → T5(max_sequence_length 可达 512),长中文不再整段喂给 CLIP。 + """ + text_full = (text_full or "").strip() + if not text_full: + d = "Chinese ink painting background, natural texture, seamless fill, no figures" + return d, d + tok = getattr(pipe, "tokenizer", None) + if tok is None: + return text_full[:120], text_full + ml = int(getattr(tok, "model_max_length", 77) or 77) + enc = tok([text_full], padding="max_length", max_length=ml, truncation=True, return_tensors="pt") + clip_s = tok.batch_decode(enc.input_ids, skip_special_tokens=True)[0].strip() + if not clip_s: + clip_s = text_full[:80] + return clip_s, text_full + + def make_flux_fill_predictor( model_id: str, device: str | None = None, @@ -69,8 +95,8 @@ def make_flux_fill_predictor( scale = max_side / float(max(orig_w, orig_h)) run_w = int(round(orig_w * scale)) run_h = int(round(orig_h * scale)) - run_w = max(8, run_w - (run_w % 8)) - run_h = max(8, run_h - (run_h % 8)) + run_w = _align_spatial_dim_for_flux_fill(run_w) + run_h = _align_spatial_dim_for_flux_fill(run_h) if (run_w, run_h) != (orig_w, orig_h): image_run = image.resize((run_w, run_h), resample=Image.BICUBIC) @@ -79,9 +105,10 @@ def make_flux_fill_predictor( image_run = image mask_run = mask - p = (prompt or "").strip() - if not p: - p = "Chinese ink painting background, natural texture, seamless fill, no figures" + p_full = (prompt or "").strip() + if not p_full: + p_full = "Chinese ink painting background, natural texture, seamless fill, no figures" + p_clip, p_t5 = _flux_clip_prompt_and_t5_full(pipe, p_full) st = float(strength) st = min(1.0, max(0.05, st)) @@ -93,7 +120,8 @@ def make_flux_fill_predictor( pass out = pipe( - prompt=p, + prompt=p_clip, + prompt_2=p_t5, image=image_run, mask_image=mask_run, height=run_h,