Add proper cancelation

This commit is contained in:
Carl Philipp Klemm 2026-03-10 00:42:26 +01:00
parent ee30799ac7
commit 6042b479a4
5 changed files with 175 additions and 234 deletions

View file

@ -8,269 +8,205 @@
#include <QDebug> #include <QDebug>
#include <QCoreApplication> #include <QCoreApplication>
#include <QRandomGenerator> #include <QRandomGenerator>
#include <cstdint>
AceStepWorker::AceStepWorker(QObject *parent) AceStep::AceStep(QObject* parent): QObject(parent)
: QObject(parent),
currentWorker(nullptr)
{ {
connect(&qwenProcess, &QProcess::finished, this, &AceStep::qwenProcFinished);
connect(&ditVaeProcess, &QProcess::finished, this, &AceStep::ditProcFinished);
} }
AceStepWorker::~AceStepWorker() bool AceStep::isGenerateing(SongItem* song)
{ {
cancelGeneration(); if(!busy && song)
*song = this->request.song;
return busy;
} }
void AceStepWorker::generateSong(const SongItem& song, const QString &jsonTemplate, void AceStep::cancleGenerateion()
const QString &aceStepPath, const QString &qwen3ModelPath,
const QString &textEncoderModelPath, const QString &ditModelPath,
const QString &vaeModelPath)
{ {
// Cancel any ongoing generation qwenProcess.blockSignals(true);
cancelGeneration(); qwenProcess.terminate();
qwenProcess.waitForFinished();
qwenProcess.blockSignals(false);
// Create worker and start it ditVaeProcess.blockSignals(true);
currentWorker = new Worker(this, song, jsonTemplate, aceStepPath, qwen3ModelPath, ditVaeProcess.terminate();
textEncoderModelPath, ditModelPath, vaeModelPath); ditVaeProcess.waitForFinished();
currentWorker->setAutoDelete(true); ditVaeProcess.blockSignals(false);
QThreadPool::globalInstance()->start(currentWorker);
progressUpdate(100);
if(busy)
generationCancled(request.song);
busy = false;
} }
void AceStepWorker::cancelGeneration() bool AceStep::requestGeneration(SongItem song, QString requestTemplate, QString aceStepPath,
QString qwen3ModelPath, QString textEncoderModelPath, QString ditModelPath,
QString vaeModelPath)
{ {
currentWorker = nullptr; if(busy)
}
bool AceStepWorker::songGenerateing(SongItem* song)
{
workerMutex.lock();
if(!currentWorker)
{ {
workerMutex.unlock(); qWarning()<<"Droping song:"<<song.caption;
return false; return false;
} }
else busy = true;
request = {song, QRandomGenerator::global()->generate(), aceStepPath, textEncoderModelPath, ditModelPath, vaeModelPath};
QString qwen3Binary = aceStepPath + "/ace-qwen3";
QFileInfo qwen3Info(qwen3Binary);
if (!qwen3Info.exists() || !qwen3Info.isExecutable())
{ {
SongItem workerSong = currentWorker->getSong(); generationError("ace-qwen3 binary not found at: " + qwen3Binary);
workerMutex.unlock(); busy = false;
if(song) return false;
*song = workerSong; }
return true; if (!QFileInfo::exists(qwen3ModelPath))
{
generationError("Qwen3 model not found: " + qwen3ModelPath);
busy = false;
return false;
}
if (!QFileInfo::exists(textEncoderModelPath))
{
generationError("Text encoder model not found: " + textEncoderModelPath);
busy = false;
return false;
}
if (!QFileInfo::exists(ditModelPath))
{
generationError("DiT model not found: " + ditModelPath);
busy = false;
return false;
}
if (!QFileInfo::exists(vaeModelPath))
{
generationError("VAE model not found: " + vaeModelPath);
busy = false;
return false;
} }
}
// Worker implementation request.requestFilePath = tempDir + "/request_" + QString::number(request.uid) + ".json";
void AceStepWorker::Worker::run()
{
uint64_t uid = QRandomGenerator::global()->generate();
// Create temporary JSON file for the request
QString tempDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
QString requestFile = tempDir + "/request_" + QString::number(uid) + ".json";
// Parse and modify the template
QJsonParseError parseError; QJsonParseError parseError;
QJsonDocument templateDoc = QJsonDocument::fromJson(jsonTemplate.toUtf8(), &parseError); QJsonDocument templateDoc = QJsonDocument::fromJson(requestTemplate.toUtf8(), &parseError);
if (!templateDoc.isObject()) if (!templateDoc.isObject())
{ {
emit parent->generationError("Invalid JSON template: " + QString(parseError.errorString())); generationError("Invalid JSON template: " + QString(parseError.errorString()));
return; busy = false;
return false;
} }
QJsonObject requestObj = templateDoc.object(); QJsonObject requestObj = templateDoc.object();
requestObj["caption"] = song.caption; requestObj["caption"] = song.caption;
if (!song.lyrics.isEmpty()) if (!song.lyrics.isEmpty())
{
requestObj["lyrics"] = song.lyrics; requestObj["lyrics"] = song.lyrics;
}
else
{
// Remove lyrics field if empty to let the LLM generate them
requestObj.remove("lyrics");
}
// Apply vocal language override if set
if (!song.vocalLanguage.isEmpty()) if (!song.vocalLanguage.isEmpty())
{
requestObj["vocal_language"] = song.vocalLanguage; requestObj["vocal_language"] = song.vocalLanguage;
}
// Write the request file // Write the request file
QFile requestFileHandle(requestFile); QFile requestFileHandle(request.requestFilePath);
if (!requestFileHandle.open(QIODevice::WriteOnly | QIODevice::Text)) if (!requestFileHandle.open(QIODevice::WriteOnly | QIODevice::Text))
{ {
emit parent->generationError("Failed to create request file: " + requestFileHandle.errorString()); emit generationError("Failed to create request file: " + requestFileHandle.errorString());
return; busy = false;
return false;
} }
requestFileHandle.write(QJsonDocument(requestObj).toJson(QJsonDocument::Indented)); requestFileHandle.write(QJsonDocument(requestObj).toJson(QJsonDocument::Indented));
requestFileHandle.close(); requestFileHandle.close();
// Use provided paths for acestep.cpp binaries QStringList qwen3Args;
QString qwen3Binary = this->aceStepPath + "/ace-qwen3"; qwen3Args << "--request" << request.requestFilePath;
QString ditVaeBinary = this->aceStepPath + "/dit-vae"; qwen3Args << "--model" << qwen3ModelPath;
// Check if binaries exist progressUpdate(30);
QFileInfo qwen3Info(qwen3Binary);
QFileInfo ditVaeInfo(ditVaeBinary);
if (!qwen3Info.exists() || !qwen3Info.isExecutable()) qwenProcess.start(qwen3Binary, qwen3Args);
return true;
}
void AceStep::qwenProcFinished(int code, QProcess::ExitStatus status)
{
QFile::remove(request.requestFilePath);
if(code != 0)
{ {
emit parent->generationError("ace-qwen3 binary not found at: " + qwen3Binary); QString errorOutput = qwenProcess.readAllStandardError();
generationError("dit-vae exited with code " + QString::number(code) + ": " + errorOutput);
busy = false;
return; return;
} }
QString ditVaeBinary = request.aceStepPath + "/dit-vae";
QFileInfo ditVaeInfo(ditVaeBinary);
if (!ditVaeInfo.exists() || !ditVaeInfo.isExecutable()) if (!ditVaeInfo.exists() || !ditVaeInfo.isExecutable())
{ {
emit parent->generationError("dit-vae binary not found at: " + ditVaeBinary); generationError("dit-vae binary not found at: " + ditVaeBinary);
busy = false;
return; return;
} }
// Use provided model paths request.requestLlmFilePath = tempDir + "/request_" + QString::number(request.uid) + "0.json";
QString qwen3Model = this->qwen3ModelPath; if (!QFileInfo::exists(request.requestLlmFilePath))
QString textEncoderModel = this->textEncoderModelPath;
QString ditModel = this->ditModelPath;
QString vaeModel = this->vaeModelPath;
if (!QFileInfo::exists(qwen3Model))
{ {
emit parent->generationError("Qwen3 model not found: " + qwen3Model); generationError("ace-qwen3 failed to create enhaced request file "+request.requestLlmFilePath);
return; busy = false;
}
if (!QFileInfo::exists(textEncoderModel))
{
emit parent->generationError("Text encoder model not found: " + textEncoderModel);
return;
}
if (!QFileInfo::exists(ditModel))
{
emit parent->generationError("DiT model not found: " + ditModel);
return;
}
if (!QFileInfo::exists(vaeModel))
{
emit parent->generationError("VAE model not found: " + vaeModel);
return;
}
// Step 1: Run ace-qwen3 to generate lyrics and audio codes
QProcess qwen3Process;
QStringList qwen3Args;
qwen3Args << "--request" << requestFile;
qwen3Args << "--model" << qwen3Model;
emit parent->progressUpdate(20);
qwen3Process.start(qwen3Binary, qwen3Args);
if (!qwen3Process.waitForStarted())
{
emit parent->generationError("Failed to start ace-qwen3: " + qwen3Process.errorString());
return;
}
if (!qwen3Process.waitForFinished(60000)) // 60 second timeout
{
qwen3Process.terminate();
qwen3Process.waitForFinished(5000);
emit parent->generationError("ace-qwen3 timed out after 60 seconds");
return;
}
int exitCode = qwen3Process.exitCode();
if (exitCode != 0)
{
QString errorOutput = qwen3Process.readAllStandardError();
emit parent->generationError("ace-qwen3 exited with code " + QString::number(exitCode) + ": " + errorOutput);
return;
}
QString requestLmOutputFile = tempDir + "/request_" + QString::number(uid) + "0.json";
if (!QFileInfo::exists(requestLmOutputFile))
{
emit parent->generationError("ace-qwen3 failed to create enhaced request file "+requestLmOutputFile);
return; return;
} }
// Load lyrics from the enhanced request file // Load lyrics from the enhanced request file
QFile lmOutputFile(requestLmOutputFile); QFile lmOutputFile(request.requestLlmFilePath);
if (lmOutputFile.open(QIODevice::ReadOnly | QIODevice::Text)) if (lmOutputFile.open(QIODevice::ReadOnly | QIODevice::Text))
{ {
QJsonParseError parseError; QJsonParseError parseError;
song.json = lmOutputFile.readAll(); request.song.json = lmOutputFile.readAll();
QJsonDocument doc = QJsonDocument::fromJson(song.json.toUtf8(), &parseError); QJsonDocument doc = QJsonDocument::fromJson(request.song.json.toUtf8(), &parseError);
lmOutputFile.close(); lmOutputFile.close();
if (doc.isObject() && !parseError.error) if (doc.isObject() && !parseError.error)
{ {
QJsonObject obj = doc.object(); QJsonObject obj = doc.object();
if (obj.contains("lyrics") && obj["lyrics"].isString()) if (obj.contains("lyrics") && obj["lyrics"].isString())
{ request.song.lyrics = obj["lyrics"].toString();
song.lyrics = obj["lyrics"].toString();
} }
} }
}
emit parent->progressUpdate(50);
// Step 2: Run dit-vae to generate audio // Step 2: Run dit-vae to generate audio
QProcess ditVaeProcess;
QStringList ditVaeArgs; QStringList ditVaeArgs;
ditVaeArgs << "--request" << requestLmOutputFile; ditVaeArgs << "--request"<<request.requestLlmFilePath;
ditVaeArgs << "--text-encoder" << textEncoderModel; ditVaeArgs << "--text-encoder"<<request.textEncoderModelPath;
ditVaeArgs << "--dit" << ditModel; ditVaeArgs << "--dit"<<request.ditModelPath;
ditVaeArgs << "--vae" << vaeModel; ditVaeArgs << "--vae"<<request.vaeModelPath;
emit parent->progressUpdate(60); progressUpdate(60);
ditVaeProcess.start(ditVaeBinary, ditVaeArgs); ditVaeProcess.start(ditVaeBinary, ditVaeArgs);
if (!ditVaeProcess.waitForStarted()) }
{
emit parent->generationError("Failed to start dit-vae: " + ditVaeProcess.errorString());
return;
}
if (!ditVaeProcess.waitForFinished(120000)) // 2 minute timeout void AceStep::ditProcFinished(int code, QProcess::ExitStatus status)
{ {
ditVaeProcess.terminate(); QFile::remove(request.requestLlmFilePath);
ditVaeProcess.waitForFinished(5000); if (code != 0)
emit parent->generationError("dit-vae timed out after 2 minutes");
return;
}
exitCode = ditVaeProcess.exitCode();
if (exitCode != 0)
{ {
QString errorOutput = ditVaeProcess.readAllStandardError(); QString errorOutput = ditVaeProcess.readAllStandardError();
emit parent->generationError("dit-vae exited with code " + QString::number(exitCode) + ": " + errorOutput); generationError("dit-vae exited with code " + QString::number(code) + ": " + errorOutput);
busy = false;
return; return;
} }
emit parent->progressUpdate(90);
// Find the generated WAV file // Find the generated WAV file
QString wavFile = QFileInfo(requestFile).absolutePath()+"/request_" + QString::number(uid) + "00.wav"; QString wavFile = tempDir+"/request_" + QString::number(request.uid) + "00.wav";
if (!QFileInfo::exists(wavFile)) if (!QFileInfo::exists(wavFile))
{ {
emit parent->generationError("No WAV file generated at "+wavFile); generationError("No WAV file generated at "+wavFile);
busy = false;
return; return;
} }
busy = false;
// Clean up temporary files progressUpdate(100);
QFile::remove(requestLmOutputFile); request.song.file = wavFile;
QFile::remove(requestFile); songGenerated(request.song);
emit parent->progressUpdate(100);
song.file = wavFile;
emit parent->songGenerated(song);
parent->workerMutex.lock();
parent->currentWorker = nullptr;
parent->workerMutex.unlock();
} }
const SongItem& AceStepWorker::Worker::getSong()
{
return song;
}

View file

@ -2,64 +2,55 @@
#define ACESTEPWORKER_H #define ACESTEPWORKER_H
#include <QObject> #include <QObject>
#include <QRunnable>
#include <QThreadPool>
#include <QString> #include <QString>
#include <QJsonObject> #include <QProcess>
#include <QMutex> #include <QStandardPaths>
#include "SongItem.h" #include "SongItem.h"
class AceStepWorker : public QObject class AceStep : public QObject
{ {
Q_OBJECT Q_OBJECT
public: QProcess qwenProcess;
explicit AceStepWorker(QObject *parent = nullptr); QProcess ditVaeProcess;
~AceStepWorker();
void generateSong(const SongItem& song, const QString &jsonTemplate, bool busy = false;
const QString &aceStepPath, const QString &qwen3ModelPath,
const QString &textEncoderModelPath, const QString &ditModelPath,
const QString &vaeModelPath);
void cancelGeneration();
bool songGenerateing(SongItem* song);
signals: struct Request
void songGenerated(const SongItem& song);
void generationFinished();
void generationError(const QString &error);
void progressUpdate(int percent);
private:
class Worker : public QRunnable
{ {
public:
Worker(AceStepWorker *parent, const SongItem& song, const QString &jsonTemplate,
const QString &aceStepPath, const QString &qwen3ModelPath,
const QString &textEncoderModelPath, const QString &ditModelPath,
const QString &vaeModelPath)
: parent(parent), song(song), jsonTemplate(jsonTemplate),
aceStepPath(aceStepPath), qwen3ModelPath(qwen3ModelPath),
textEncoderModelPath(textEncoderModelPath), ditModelPath(ditModelPath),
vaeModelPath(vaeModelPath) {}
void run() override;
const SongItem& getSong();
private:
AceStepWorker *parent;
SongItem song; SongItem song;
QString jsonTemplate; uint64_t uid;
QString aceStepPath; QString aceStepPath;
QString qwen3ModelPath;
QString textEncoderModelPath; QString textEncoderModelPath;
QString ditModelPath; QString ditModelPath;
QString vaeModelPath; QString vaeModelPath;
QString requestFilePath;
QString requestLlmFilePath;
}; };
QMutex workerMutex; Request request;
Worker *currentWorker;
const QString tempDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
signals:
void songGenerated(SongItem song);
void generationCancled(SongItem song);
void generationError(QString error);
void progressUpdate(int progress);
public slots:
bool requestGeneration(SongItem song, QString requestTemplate, QString aceStepPath,
QString qwen3ModelPath, QString textEncoderModelPath, QString ditModelPath,
QString vaeModelPath);
public:
AceStep(QObject* parent = nullptr);
bool isGenerateing(SongItem* song = nullptr);
void cancleGenerateion();
private slots:
void qwenProcFinished(int code, QProcess::ExitStatus status);
void ditProcFinished(int code, QProcess::ExitStatus status);
}; };
#endif // ACESTEPWORKER_H #endif // ACESTEPWORKER_H

View file

@ -18,13 +18,15 @@ MainWindow::MainWindow(QWidget *parent)
ui(new Ui::MainWindow), ui(new Ui::MainWindow),
songModel(new SongListModel(this)), songModel(new SongListModel(this)),
audioPlayer(new AudioPlayer(this)), audioPlayer(new AudioPlayer(this)),
aceStepWorker(new AceStepWorker(this)), aceStep(new AceStep(this)),
playbackTimer(new QTimer(this)), playbackTimer(new QTimer(this)),
isPlaying(false), isPlaying(false),
isPaused(false), isPaused(false),
shuffleMode(false), shuffleMode(false),
isGeneratingNext(false) isGeneratingNext(false)
{ {
aceStep->moveToThread(&aceThread);
ui->setupUi(this); ui->setupUi(this);
// Setup lyrics display // Setup lyrics display
@ -57,9 +59,10 @@ MainWindow::MainWindow(QWidget *parent)
connect(audioPlayer, &AudioPlayer::playbackStarted, this, &MainWindow::playbackStarted); connect(audioPlayer, &AudioPlayer::playbackStarted, this, &MainWindow::playbackStarted);
connect(audioPlayer, &AudioPlayer::positionChanged, this, &MainWindow::updatePosition); connect(audioPlayer, &AudioPlayer::positionChanged, this, &MainWindow::updatePosition);
connect(audioPlayer, &AudioPlayer::durationChanged, this, &MainWindow::updateDuration); connect(audioPlayer, &AudioPlayer::durationChanged, this, &MainWindow::updateDuration);
connect(aceStepWorker, &AceStepWorker::songGenerated, this, &MainWindow::songGenerated); connect(aceStep, &AceStep::songGenerated, this, &MainWindow::songGenerated);
connect(aceStepWorker, &AceStepWorker::generationError, this, &MainWindow::generationError); connect(aceStep, &AceStep::generationCancled, this, &MainWindow::generationCanceld);
connect(aceStepWorker, &AceStepWorker::progressUpdate, ui->progressBar, &QProgressBar::setValue); connect(aceStep, &AceStep::generationError, this, &MainWindow::generationError);
connect(aceStep, &AceStep::progressUpdate, ui->progressBar, &QProgressBar::setValue);
// Connect double-click on song list for editing (works with QTableView too) // Connect double-click on song list for editing (works with QTableView too)
connect(ui->songListView, &QTableView::doubleClicked, this, &MainWindow::on_songListView_doubleClicked); connect(ui->songListView, &QTableView::doubleClicked, this, &MainWindow::on_songListView_doubleClicked);
@ -269,6 +272,10 @@ void MainWindow::on_shuffleButton_clicked()
{ {
shuffleMode = ui->shuffleButton->isChecked(); shuffleMode = ui->shuffleButton->isChecked();
updateControls(); updateControls();
flushGenerationQueue();
if(isPlaying)
ensureSongsInQueue();
} }
void MainWindow::on_addSongButton_clicked() void MainWindow::on_addSongButton_clicked()
@ -426,6 +433,11 @@ void MainWindow::songGenerated(const SongItem& song)
ensureSongsInQueue(); ensureSongsInQueue();
} }
void MainWindow::generationCanceld(const SongItem& song)
{
ui->statusbar->showMessage("Geneartion cancled: " + song.caption);
}
void MainWindow::playNextSong() void MainWindow::playNextSong()
{ {
if (!isPlaying) if (!isPlaying)
@ -505,7 +517,7 @@ void MainWindow::ensureSongsInQueue(bool enqeueCurrent)
SongItem lastSong; SongItem lastSong;
SongItem workerSong; SongItem workerSong;
if(aceStepWorker->songGenerateing(&workerSong)) if(aceStep->isGenerateing(&workerSong))
lastSong = workerSong; lastSong = workerSong;
else if(!generatedSongQueue.empty()) else if(!generatedSongQueue.empty())
lastSong = generatedSongQueue.last(); lastSong = generatedSongQueue.last();
@ -526,7 +538,7 @@ void MainWindow::ensureSongsInQueue(bool enqeueCurrent)
isGeneratingNext = true; isGeneratingNext = true;
ui->statusbar->showMessage("Generateing: "+nextSong.caption); ui->statusbar->showMessage("Generateing: "+nextSong.caption);
aceStepWorker->generateSong(nextSong, jsonTemplate, aceStep->requestGeneration(nextSong, jsonTemplate,
aceStepPath, qwen3ModelPath, aceStepPath, qwen3ModelPath,
textEncoderModelPath, ditModelPath, textEncoderModelPath, ditModelPath,
vaeModelPath); vaeModelPath);
@ -535,7 +547,7 @@ void MainWindow::ensureSongsInQueue(bool enqeueCurrent)
void MainWindow::flushGenerationQueue() void MainWindow::flushGenerationQueue()
{ {
generatedSongQueue.clear(); generatedSongQueue.clear();
aceStepWorker->cancelGeneration(); aceStep->cancleGenerateion();
isGeneratingNext = false; isGeneratingNext = false;
} }

View file

@ -7,7 +7,7 @@
#include <QTimer> #include <QTimer>
#include <QQueue> #include <QQueue>
#include <QPair> #include <QPair>
#include <cstdint> #include <QThread>
#include <QStandardPaths> #include <QStandardPaths>
#include <QJsonArray> #include <QJsonArray>
#include <QJsonObject> #include <QJsonObject>
@ -47,6 +47,7 @@ private slots:
void on_songListView_doubleClicked(const QModelIndex &index); void on_songListView_doubleClicked(const QModelIndex &index);
void songGenerated(const SongItem& song); void songGenerated(const SongItem& song);
void generationCanceld(const SongItem& song);
void playNextSong(); void playNextSong();
void playbackStarted(); void playbackStarted();
void updatePlaybackStatus(bool playing); void updatePlaybackStatus(bool playing);
@ -64,7 +65,8 @@ private:
Ui::MainWindow *ui; Ui::MainWindow *ui;
SongListModel *songModel; SongListModel *songModel;
AudioPlayer *audioPlayer; AudioPlayer *audioPlayer;
AceStepWorker *aceStepWorker; QThread aceThread;
AceStep *aceStep;
QTimer *playbackTimer; QTimer *playbackTimer;
QString formatTime(int milliseconds); QString formatTime(int milliseconds);

View file

@ -1,7 +1,7 @@
#include "MainWindow.h"
#include <QApplication> #include <QApplication>
#include <QStyleFactory> #include <QThread>
#include <QIcon>
#include "MainWindow.h"
int main(int argc, char *argv[]) int main(int argc, char *argv[])
{ {