update
This commit is contained in:
@@ -1,29 +1,45 @@
|
||||
#include "dialogs/FrameAnimationDialog.h"
|
||||
|
||||
#include "core/animation/AnimationSampling.h"
|
||||
#include "core/image/ImageDecodeConfig.h"
|
||||
#include "core/image/ImageFileLoader.h"
|
||||
#include "core/net/ModelServerClient.h"
|
||||
#include "core/workspace/ProjectWorkspace.h"
|
||||
#include "dialogs/CancelableTaskDialog.h"
|
||||
|
||||
#include <QBoxLayout>
|
||||
#include <QAbstractItemView>
|
||||
#include <QBuffer>
|
||||
#include <QColor>
|
||||
#include <QColorDialog>
|
||||
#include <QDialog>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QDir>
|
||||
#include <QFormLayout>
|
||||
#include <QFileDialog>
|
||||
#include <QFileInfo>
|
||||
#include <QImage>
|
||||
#include <QImageReader>
|
||||
#include <QIODevice>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QLabel>
|
||||
#include <QLineEdit>
|
||||
#include <QListWidget>
|
||||
#include <QMessageBox>
|
||||
#include <QNetworkReply>
|
||||
#include <QNetworkRequest>
|
||||
#include <QPixmap>
|
||||
#include <QPushButton>
|
||||
#include <QUrl>
|
||||
|
||||
namespace {
|
||||
|
||||
QString resolvedImageAbsForFrame(const core::ProjectWorkspace& ws,
|
||||
const core::Project::Entity& e,
|
||||
int frame) {
|
||||
const QString rel = core::sampleImagePath(e.imageFrames, frame, e.imagePath);
|
||||
if (rel.isEmpty()) return {};
|
||||
const QString abs = QDir(ws.projectDir()).filePath(rel);
|
||||
return abs;
|
||||
const QByteArray png = ws.entityImagePngAt(e.id, frame);
|
||||
if (png.isEmpty()) return {};
|
||||
return QStringLiteral("pngb64:") + QString::fromLatin1(png.toBase64());
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -56,6 +72,7 @@ FrameAnimationDialog::FrameAnimationDialog(core::ProjectWorkspace& workspace,
|
||||
|
||||
m_list = new QListWidget(this);
|
||||
m_list->setMinimumWidth(240);
|
||||
m_list->setSelectionMode(QAbstractItemView::ExtendedSelection);
|
||||
mid->addWidget(m_list, 0);
|
||||
|
||||
auto* right = new QVBoxLayout();
|
||||
@@ -71,18 +88,27 @@ FrameAnimationDialog::FrameAnimationDialog(core::ProjectWorkspace& workspace,
|
||||
auto* row = new QHBoxLayout();
|
||||
right->addLayout(row);
|
||||
m_btnReplace = new QPushButton(QStringLiteral("替换此帧…"), this);
|
||||
m_btnClear = new QPushButton(QStringLiteral("清除此帧(恢复默认)"), this);
|
||||
m_btnClear = new QPushButton(QStringLiteral("清除选中帧"), this);
|
||||
row->addWidget(m_btnReplace);
|
||||
row->addWidget(m_btnClear);
|
||||
|
||||
auto* row2 = new QHBoxLayout();
|
||||
right->addLayout(row2);
|
||||
m_btnImportFiles = new QPushButton(QStringLiteral("批量导入(多选图片)…"), this);
|
||||
m_btnImportFolder = new QPushButton(QStringLiteral("批量导入(文件夹)…"), this);
|
||||
m_btnImportFiles = new QPushButton(QStringLiteral("导入图片…"), this);
|
||||
m_btnImportFolder = new QPushButton(QStringLiteral("导入文件夹…"), this);
|
||||
row2->addWidget(m_btnImportFiles);
|
||||
row2->addWidget(m_btnImportFolder);
|
||||
row2->addStretch(1);
|
||||
|
||||
auto* row3 = new QHBoxLayout();
|
||||
right->addLayout(row3);
|
||||
m_btnAiGenerate = new QPushButton(QStringLiteral("AI 生成逐帧动画…"), this);
|
||||
row3->addWidget(m_btnAiGenerate);
|
||||
auto* aiHint = new QLabel(QStringLiteral("写入选中区间"), this);
|
||||
aiHint->setStyleSheet(QStringLiteral("color: #777;"));
|
||||
row3->addWidget(aiHint);
|
||||
row3->addStretch(1);
|
||||
|
||||
auto* closeRow = new QHBoxLayout();
|
||||
root->addLayout(closeRow);
|
||||
closeRow->addStretch(1);
|
||||
@@ -95,6 +121,7 @@ FrameAnimationDialog::FrameAnimationDialog(core::ProjectWorkspace& workspace,
|
||||
connect(m_btnClear, &QPushButton::clicked, this, &FrameAnimationDialog::onClearCurrentFrame);
|
||||
connect(m_btnImportFiles, &QPushButton::clicked, this, &FrameAnimationDialog::onBatchImportFiles);
|
||||
connect(m_btnImportFolder, &QPushButton::clicked, this, &FrameAnimationDialog::onBatchImportFolder);
|
||||
connect(m_btnAiGenerate, &QPushButton::clicked, this, &FrameAnimationDialog::onGenerateAiFrames);
|
||||
|
||||
rebuildFrameList();
|
||||
if (m_list->count() > 0) {
|
||||
@@ -118,16 +145,11 @@ void FrameAnimationDialog::rebuildFrameList() {
|
||||
|
||||
// 默认贴图(用于 UI 提示)
|
||||
m_defaultImageAbs.clear();
|
||||
if (!hit->imagePath.isEmpty()) {
|
||||
const QString abs = QDir(m_workspace.projectDir()).filePath(hit->imagePath);
|
||||
if (QFileInfo::exists(abs)) {
|
||||
m_defaultImageAbs = abs;
|
||||
}
|
||||
}
|
||||
|
||||
for (int f = m_start; f <= m_end; ++f) {
|
||||
bool hasCustom = false;
|
||||
for (const auto& k : hit->imageFrames) {
|
||||
const auto spriteFrames = m_workspace.entitySpriteFrames(m_entityId);
|
||||
for (const auto& k : spriteFrames) {
|
||||
if (k.frame == f) {
|
||||
hasCustom = true;
|
||||
break;
|
||||
@@ -159,38 +181,33 @@ void FrameAnimationDialog::updatePreviewForFrame(int frame) {
|
||||
if (!hit) return;
|
||||
|
||||
const QString abs = resolvedImageAbsForFrame(m_workspace, *hit, frame);
|
||||
if (abs.isEmpty() || !QFileInfo::exists(abs)) {
|
||||
if (abs.isEmpty()) {
|
||||
m_preview->setText(QStringLiteral("无图像"));
|
||||
return;
|
||||
}
|
||||
QPixmap pm(abs);
|
||||
if (pm.isNull()) {
|
||||
QImage loaded;
|
||||
if (abs.startsWith(QStringLiteral("pngb64:"))) {
|
||||
const QByteArray b64 = abs.mid(QStringLiteral("pngb64:").size()).toLatin1();
|
||||
const QByteArray raw = QByteArray::fromBase64(b64);
|
||||
loaded.loadFromData(raw, "PNG");
|
||||
} else {
|
||||
if (!QFileInfo::exists(abs)) {
|
||||
m_preview->setText(QStringLiteral("无图像"));
|
||||
return;
|
||||
}
|
||||
loaded = core::image_file::loadImageLimited(abs, core::image_decode::kPreviewMaxPixels);
|
||||
}
|
||||
if (loaded.isNull()) {
|
||||
m_preview->setText(QStringLiteral("加载失败"));
|
||||
return;
|
||||
}
|
||||
QPixmap pm = QPixmap::fromImage(loaded);
|
||||
m_preview->setPixmap(pm.scaled(m_preview->size(), Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
||||
}
|
||||
|
||||
bool FrameAnimationDialog::applyImageToFrame(int frame, const QString& absImagePath) {
|
||||
// Qt 默认的 image allocation limit 较小,超大分辨率图可能会被拒绝。
|
||||
// 这里提高 limit,并对极端大图按像素数上限自动缩放,避免 OOM。
|
||||
#if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0)
|
||||
QImageReader::setAllocationLimit(1024); // MB
|
||||
#endif
|
||||
QImageReader reader(absImagePath);
|
||||
reader.setAutoTransform(true);
|
||||
const QSize sz = reader.size();
|
||||
if (sz.isValid()) {
|
||||
constexpr qint64 kMaxPixels = 120LL * 1000LL * 1000LL; // 120MP
|
||||
const qint64 pixels = qint64(sz.width()) * qint64(sz.height());
|
||||
if (pixels > kMaxPixels) {
|
||||
const double s = std::sqrt(double(kMaxPixels) / std::max<double>(1.0, double(pixels)));
|
||||
const int nw = std::max(1, int(std::lround(sz.width() * s)));
|
||||
const int nh = std::max(1, int(std::lround(sz.height() * s)));
|
||||
reader.setScaledSize(QSize(nw, nh));
|
||||
}
|
||||
}
|
||||
QImage img = reader.read();
|
||||
QImage img =
|
||||
core::image_file::loadImageLimited(absImagePath, core::image_decode::kWorkspaceMaxPixels);
|
||||
if (img.isNull()) {
|
||||
return false;
|
||||
}
|
||||
@@ -200,6 +217,93 @@ bool FrameAnimationDialog::applyImageToFrame(int frame, const QString& absImageP
|
||||
return m_workspace.setEntityImageFrame(m_entityId, frame, img);
|
||||
}
|
||||
|
||||
QVector<int> FrameAnimationDialog::targetFramesForWrite(int requestedCount) const {
|
||||
QVector<int> frames;
|
||||
if (m_list) {
|
||||
const auto selected = m_list->selectedItems();
|
||||
frames.reserve(selected.size());
|
||||
for (auto* it : selected) {
|
||||
if (it) frames.push_back(it->data(Qt::UserRole).toInt());
|
||||
}
|
||||
std::sort(frames.begin(), frames.end());
|
||||
frames.erase(std::unique(frames.begin(), frames.end()), frames.end());
|
||||
}
|
||||
if (!frames.isEmpty()) {
|
||||
if (requestedCount > 0 && frames.size() > requestedCount) {
|
||||
frames.resize(requestedCount);
|
||||
}
|
||||
return frames;
|
||||
}
|
||||
|
||||
const int count = requestedCount > 0 ? std::min(requestedCount, m_end - m_start + 1)
|
||||
: (m_end - m_start + 1);
|
||||
frames.reserve(count);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
frames.push_back(m_start + i);
|
||||
}
|
||||
return frames;
|
||||
}
|
||||
|
||||
QByteArray FrameAnimationDialog::currentEntityPngBytes(QString* outError) const {
|
||||
if (outError) outError->clear();
|
||||
if (!m_workspace.isOpen()) {
|
||||
if (outError) *outError = QStringLiteral("未打开项目。");
|
||||
return {};
|
||||
}
|
||||
|
||||
const auto& ents = m_workspace.entities();
|
||||
const core::Project::Entity* hit = nullptr;
|
||||
for (const auto& e : ents) {
|
||||
if (e.id == m_entityId) {
|
||||
hit = &e;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!hit) {
|
||||
if (outError) *outError = QStringLiteral("未找到当前实体。");
|
||||
return {};
|
||||
}
|
||||
|
||||
int sourceFrame = m_start;
|
||||
if (m_list && m_list->currentItem()) {
|
||||
sourceFrame = m_list->currentItem()->data(Qt::UserRole).toInt();
|
||||
}
|
||||
|
||||
const QString abs = resolvedImageAbsForFrame(m_workspace, *hit, sourceFrame);
|
||||
if (abs.isEmpty()) {
|
||||
if (outError) *outError = QStringLiteral("当前实体没有可用的角色贴图。");
|
||||
return {};
|
||||
}
|
||||
|
||||
QImage img;
|
||||
if (abs.startsWith(QStringLiteral("pngb64:"))) {
|
||||
const QByteArray b64 = abs.mid(QStringLiteral("pngb64:").size()).toLatin1();
|
||||
const QByteArray raw = QByteArray::fromBase64(b64);
|
||||
img.loadFromData(raw, "PNG");
|
||||
} else {
|
||||
if (!QFileInfo::exists(abs)) {
|
||||
if (outError) *outError = QStringLiteral("当前实体没有可用的角色贴图。");
|
||||
return {};
|
||||
}
|
||||
img = core::image_file::loadImageLimited(abs, core::image_decode::kWorkspaceMaxPixels);
|
||||
}
|
||||
if (img.isNull()) {
|
||||
if (outError) *outError = QStringLiteral("角色贴图读取失败。");
|
||||
return {};
|
||||
}
|
||||
if (img.format() != QImage::Format_ARGB32 && img.format() != QImage::Format_ARGB32_Premultiplied) {
|
||||
img = img.convertToFormat(QImage::Format_ARGB32);
|
||||
}
|
||||
|
||||
QByteArray png;
|
||||
QBuffer buf(&png);
|
||||
if (!buf.open(QIODevice::WriteOnly) || !img.save(&buf, "PNG")) {
|
||||
if (outError) *outError = QStringLiteral("角色贴图编码 PNG 失败。");
|
||||
return {};
|
||||
}
|
||||
return png;
|
||||
}
|
||||
|
||||
void FrameAnimationDialog::onReplaceCurrentFrame() {
|
||||
auto* it = m_list->currentItem();
|
||||
if (!it) return;
|
||||
@@ -222,15 +326,17 @@ void FrameAnimationDialog::onReplaceCurrentFrame() {
|
||||
}
|
||||
if (hit) {
|
||||
// 是否已有精确关键帧
|
||||
for (const auto& k : hit->imageFrames) {
|
||||
const auto spriteFrames = m_workspace.entitySpriteFrames(m_entityId);
|
||||
for (const auto& k : spriteFrames) {
|
||||
if (k.frame == f + 1) {
|
||||
hasExplicitKeyAtFPlus1 = true;
|
||||
}
|
||||
}
|
||||
prevRelPathForF = core::sampleImagePath(hit->imageFrames, f, hit->imagePath);
|
||||
prevRelPathForFPlus1 = core::sampleImagePath(hit->imageFrames, f + 1, hit->imagePath);
|
||||
prevRelPathForF = m_workspace.entityImagePathAt(m_entityId, f);
|
||||
prevRelPathForFPlus1 = m_workspace.entityImagePathAt(m_entityId, f + 1);
|
||||
}
|
||||
}
|
||||
(void)prevRelPathForF;
|
||||
|
||||
const QString path = QFileDialog::getOpenFileName(
|
||||
this,
|
||||
@@ -255,14 +361,12 @@ void FrameAnimationDialog::onReplaceCurrentFrame() {
|
||||
}
|
||||
|
||||
void FrameAnimationDialog::onClearCurrentFrame() {
|
||||
auto* it = m_list->currentItem();
|
||||
if (!it) return;
|
||||
const int f = it->data(Qt::UserRole).toInt();
|
||||
if (!m_workspace.removeEntityImageFrame(m_entityId, f)) {
|
||||
return;
|
||||
const QVector<int> frames = targetFramesForWrite(0);
|
||||
for (int f : frames) {
|
||||
m_workspace.removeEntityImageFrame(m_entityId, f);
|
||||
}
|
||||
rebuildFrameList();
|
||||
updatePreviewForFrame(f);
|
||||
onSelectFrame();
|
||||
}
|
||||
|
||||
void FrameAnimationDialog::onBatchImportFiles() {
|
||||
@@ -274,10 +378,10 @@ void FrameAnimationDialog::onBatchImportFiles() {
|
||||
if (paths.isEmpty()) return;
|
||||
QStringList sorted = paths;
|
||||
sorted.sort(Qt::CaseInsensitive);
|
||||
const int need = m_end - m_start + 1;
|
||||
const int count = std::min(need, static_cast<int>(sorted.size()));
|
||||
const QVector<int> targets = targetFramesForWrite(static_cast<int>(sorted.size()));
|
||||
const int count = std::min(static_cast<int>(targets.size()), static_cast<int>(sorted.size()));
|
||||
for (int i = 0; i < count; ++i) {
|
||||
applyImageToFrame(m_start + i, sorted[i]);
|
||||
applyImageToFrame(targets[i], sorted[i]);
|
||||
}
|
||||
rebuildFrameList();
|
||||
onSelectFrame();
|
||||
@@ -296,12 +400,213 @@ void FrameAnimationDialog::onBatchImportFolder() {
|
||||
QStringLiteral("*.webp")};
|
||||
const QStringList files = d.entryList(filters, QDir::Files, QDir::Name);
|
||||
if (files.isEmpty()) return;
|
||||
const int need = m_end - m_start + 1;
|
||||
const int count = std::min(need, static_cast<int>(files.size()));
|
||||
const QVector<int> targets = targetFramesForWrite(static_cast<int>(files.size()));
|
||||
const int count = std::min(static_cast<int>(targets.size()), static_cast<int>(files.size()));
|
||||
for (int i = 0; i < count; ++i) {
|
||||
applyImageToFrame(m_start + i, d.filePath(files[i]));
|
||||
applyImageToFrame(targets[i], d.filePath(files[i]));
|
||||
}
|
||||
rebuildFrameList();
|
||||
onSelectFrame();
|
||||
}
|
||||
|
||||
void FrameAnimationDialog::onGenerateAiFrames() {
|
||||
const QVector<int> targetFrames = targetFramesForWrite(0);
|
||||
const int frameCount = static_cast<int>(targetFrames.size());
|
||||
if (frameCount <= 0) {
|
||||
QMessageBox::warning(this, QStringLiteral("AI 生成动画"), QStringLiteral("当前区间无有效帧。"));
|
||||
return;
|
||||
}
|
||||
|
||||
QString imageErr;
|
||||
const QByteArray characterPng = currentEntityPngBytes(&imageErr);
|
||||
if (characterPng.isEmpty()) {
|
||||
QMessageBox::warning(this,
|
||||
QStringLiteral("AI 生成动画"),
|
||||
imageErr.isEmpty() ? QStringLiteral("无法读取当前实体贴图。") : imageErr);
|
||||
return;
|
||||
}
|
||||
|
||||
QDialog cfg(this);
|
||||
cfg.setWindowTitle(QStringLiteral("AI 生成实体动画"));
|
||||
cfg.setModal(true);
|
||||
auto* root = new QVBoxLayout(&cfg);
|
||||
root->setContentsMargins(14, 14, 14, 14);
|
||||
root->setSpacing(10);
|
||||
|
||||
auto* form = new QFormLayout();
|
||||
root->addLayout(form);
|
||||
|
||||
auto* promptEdit = new QLineEdit(QStringLiteral("walk cycle"), &cfg);
|
||||
form->addRow(QStringLiteral("动画提示词"), promptEdit);
|
||||
|
||||
auto* bgRow = new QWidget(&cfg);
|
||||
auto* bgLayout = new QHBoxLayout(bgRow);
|
||||
bgLayout->setContentsMargins(0, 0, 0, 0);
|
||||
auto* bgEdit = new QLineEdit(QStringLiteral("#00FF00"), bgRow);
|
||||
auto* bgBtn = new QPushButton(QStringLiteral("选择…"), bgRow);
|
||||
bgLayout->addWidget(bgEdit, 1);
|
||||
bgLayout->addWidget(bgBtn);
|
||||
form->addRow(QStringLiteral("生成背景色"), bgRow);
|
||||
|
||||
auto* frameLabel = new QLabel(QString::number(frameCount), &cfg);
|
||||
form->addRow(QStringLiteral("帧数"), frameLabel);
|
||||
|
||||
auto* buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, &cfg);
|
||||
buttons->button(QDialogButtonBox::Ok)->setText(QStringLiteral("开始生成"));
|
||||
buttons->button(QDialogButtonBox::Cancel)->setText(QStringLiteral("取消"));
|
||||
root->addWidget(buttons);
|
||||
|
||||
connect(bgBtn, &QPushButton::clicked, &cfg, [bgEdit, &cfg]() {
|
||||
const QColor initial(bgEdit->text().trimmed());
|
||||
const QColor c = QColorDialog::getColor(initial.isValid() ? initial : QColor(0, 255, 0),
|
||||
&cfg,
|
||||
QStringLiteral("选择纯色背景"));
|
||||
if (c.isValid()) {
|
||||
bgEdit->setText(c.name(QColor::HexRgb).toUpper());
|
||||
}
|
||||
});
|
||||
connect(buttons, &QDialogButtonBox::accepted, &cfg, &QDialog::accept);
|
||||
connect(buttons, &QDialogButtonBox::rejected, &cfg, &QDialog::reject);
|
||||
|
||||
if (cfg.exec() != QDialog::Accepted) {
|
||||
return;
|
||||
}
|
||||
const QString prompt = promptEdit->text().trimmed();
|
||||
if (prompt.isEmpty()) {
|
||||
QMessageBox::warning(this, QStringLiteral("AI 生成动画"), QStringLiteral("动画提示词不能为空。"));
|
||||
return;
|
||||
}
|
||||
|
||||
QString base;
|
||||
const QByteArray env = qgetenv("MODEL_SERVER_URL");
|
||||
base = env.isEmpty() ? QStringLiteral("http://127.0.0.1:8000") : QString::fromUtf8(env);
|
||||
|
||||
auto* client = new core::ModelServerClient(this);
|
||||
client->setBaseUrl(QUrl(base));
|
||||
|
||||
QString immediateErr;
|
||||
QNetworkReply* reply = client->animateCharacterSequenceAsync(
|
||||
characterPng,
|
||||
prompt,
|
||||
QStringLiteral("blur, distortion, extra limbs, bad hands, text, watermark"),
|
||||
bgEdit->text(),
|
||||
40,
|
||||
frameCount,
|
||||
25,
|
||||
8.0,
|
||||
768,
|
||||
-1,
|
||||
&immediateErr);
|
||||
if (!reply) {
|
||||
QMessageBox::warning(this,
|
||||
QStringLiteral("AI 生成动画"),
|
||||
immediateErr.isEmpty() ? QStringLiteral("无法发起后端请求。") : immediateErr);
|
||||
client->deleteLater();
|
||||
return;
|
||||
}
|
||||
|
||||
auto* task = new CancelableTaskDialog(
|
||||
QStringLiteral("AI 生成实体动画"),
|
||||
QStringLiteral("正在生成 PNG 序列。该过程可能耗时较久,请稍候……"),
|
||||
this);
|
||||
task->setAttribute(Qt::WA_DeleteOnClose, true);
|
||||
connect(task, &CancelableTaskDialog::canceled, this, [reply, task]() {
|
||||
if (reply) reply->abort();
|
||||
if (task) task->reject();
|
||||
});
|
||||
|
||||
connect(reply, &QNetworkReply::finished, this, [this, reply, task, client, frameCount, targetFrames]() {
|
||||
const int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
|
||||
const QByteArray raw = reply->readAll();
|
||||
const auto netErr = reply->error();
|
||||
const QString netErrStr = reply->errorString();
|
||||
|
||||
reply->deleteLater();
|
||||
client->deleteLater();
|
||||
if (task) task->close();
|
||||
|
||||
if (netErr != QNetworkReply::NoError) {
|
||||
QMessageBox::warning(this,
|
||||
QStringLiteral("AI 生成动画"),
|
||||
QStringLiteral("网络错误:%1").arg(netErrStr));
|
||||
return;
|
||||
}
|
||||
if (httpStatus != 200) {
|
||||
QString detail;
|
||||
const QJsonDocument jerr = QJsonDocument::fromJson(raw);
|
||||
if (jerr.isObject()) {
|
||||
detail = jerr.object().value(QStringLiteral("detail")).toString();
|
||||
}
|
||||
QMessageBox::warning(this,
|
||||
QStringLiteral("AI 生成动画"),
|
||||
detail.isEmpty() ? QStringLiteral("后端返回 HTTP %1。").arg(httpStatus)
|
||||
: QStringLiteral("后端错误(HTTP %1):%2").arg(httpStatus).arg(detail));
|
||||
return;
|
||||
}
|
||||
|
||||
const QJsonDocument jd = QJsonDocument::fromJson(raw);
|
||||
if (!jd.isObject()) {
|
||||
QMessageBox::warning(this, QStringLiteral("AI 生成动画"), QStringLiteral("响应不是 JSON。"));
|
||||
return;
|
||||
}
|
||||
const QJsonObject obj = jd.object();
|
||||
if (!obj.value(QStringLiteral("success")).toBool()) {
|
||||
const QString err = obj.value(QStringLiteral("error")).toString();
|
||||
QMessageBox::warning(this,
|
||||
QStringLiteral("AI 生成动画"),
|
||||
err.isEmpty() ? QStringLiteral("生成失败。") : err);
|
||||
return;
|
||||
}
|
||||
|
||||
const QJsonArray frames = obj.value(QStringLiteral("frames")).toArray();
|
||||
if (frames.isEmpty()) {
|
||||
QMessageBox::warning(this, QStringLiteral("AI 生成动画"), QStringLiteral("后端未返回 PNG 帧。"));
|
||||
return;
|
||||
}
|
||||
|
||||
int applied = 0;
|
||||
const int count = std::min(frameCount, static_cast<int>(frames.size()));
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const QJsonObject fobj = frames.at(i).toObject();
|
||||
const QString b64 = fobj.value(QStringLiteral("image_b64")).toString();
|
||||
if (b64.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QImage img;
|
||||
img.loadFromData(QByteArray::fromBase64(b64.toLatin1()), "PNG");
|
||||
if (img.isNull()) {
|
||||
continue;
|
||||
}
|
||||
if (img.format() != QImage::Format_ARGB32_Premultiplied) {
|
||||
img = img.convertToFormat(QImage::Format_ARGB32_Premultiplied);
|
||||
}
|
||||
if (i < targetFrames.size() && m_workspace.setEntityImageFrame(m_entityId, targetFrames[i], img)) {
|
||||
++applied;
|
||||
}
|
||||
}
|
||||
|
||||
rebuildFrameList();
|
||||
if (m_list && m_list->count() > 0) {
|
||||
m_list->setCurrentRow(0);
|
||||
}
|
||||
onSelectFrame();
|
||||
|
||||
if (applied <= 0) {
|
||||
QMessageBox::warning(this, QStringLiteral("AI 生成动画"), QStringLiteral("生成成功,但写入项目失败。"));
|
||||
return;
|
||||
}
|
||||
if (applied < frameCount) {
|
||||
QMessageBox::information(this,
|
||||
QStringLiteral("AI 生成动画"),
|
||||
QStringLiteral("已写入 %1/%2 帧;部分帧解析或保存失败。").arg(applied).arg(frameCount));
|
||||
} else {
|
||||
QMessageBox::information(this,
|
||||
QStringLiteral("AI 生成动画"),
|
||||
QStringLiteral("已写入 %1 帧。").arg(applied));
|
||||
}
|
||||
});
|
||||
|
||||
task->show();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user