Compare commits
35 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e4b5f9af5e | |||
| 7edd7660a6 | |||
| 8762983cc7 | |||
| 01b5b160ba | |||
| e8fce10c0d | |||
| b3185e4bc5 | |||
| 35d87197dc | |||
| ac29d2629b | |||
| c7f5c97e7b | |||
| f85bbb326e | |||
| cc72f360b9 | |||
| e3fb4761b0 | |||
| 55be24b36f | |||
| be21c1f2bd | |||
| ef17335614 | |||
| 39f6429199 | |||
| 275c1a1852 | |||
| 216e59c105 | |||
| 14dec9f335 | |||
| de7207f07e | |||
| 6be80f1d5c | |||
| 6ef8591769 | |||
| 9e0b8b4197 | |||
| 4cacaa04e4 | |||
| 64357be451 | |||
| 9d0dc6298c | |||
|
|
970ed46892 | ||
| 2de2d1bc6b | |||
|
|
cb096388c8 | ||
|
|
227ee981a3 | ||
|
|
6297477e29 | ||
|
|
61003eb84c | ||
|
|
6db87d119d | ||
| 6f36719795 | |||
| 06d15ecfef |
34 changed files with 2730 additions and 720 deletions
15
.copywrite.hcl
Normal file
15
.copywrite.hcl
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
schema_version = 1
|
||||
|
||||
project {
|
||||
license = "GPL-3.0-or-later"
|
||||
copyright_year = 2026
|
||||
copyright_holder = "Carl Philipp Klemm"
|
||||
|
||||
# (OPTIONAL) A list of globs that should not have copyright/license headers.
|
||||
# Supports doublestar glob patterns for more flexibility in defining which
|
||||
# files or folders should be ignored
|
||||
header_ignore = [
|
||||
# "vendor/**",
|
||||
# "**autogen**",
|
||||
]
|
||||
}
|
||||
20
.github/actions/linux-setup-vulkan/action.yml
vendored
Normal file
20
.github/actions/linux-setup-vulkan/action.yml
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
name: "Linux - Setup Vulkan SDK"
|
||||
description: "Setup Vulkan SDK for Linux"
|
||||
inputs:
|
||||
path:
|
||||
description: "Installation path"
|
||||
required: true
|
||||
version:
|
||||
description: "Vulkan SDK version"
|
||||
required: true
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Setup Vulkan SDK
|
||||
id: setup
|
||||
uses: ./.github/actions/unarchive-tar
|
||||
with:
|
||||
url: https://sdk.lunarg.com/sdk/download/${{ inputs.version }}/linux/vulkan_sdk.tar.xz
|
||||
path: ${{ inputs.path }}
|
||||
strip: 1
|
||||
27
.github/actions/unarchive-tar/action.yml
vendored
Normal file
27
.github/actions/unarchive-tar/action.yml
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
name: "Unarchive tar"
|
||||
description: "Download and unarchive tar into directory"
|
||||
inputs:
|
||||
url:
|
||||
description: "URL of the tar archive"
|
||||
required: true
|
||||
path:
|
||||
description: "Directory to unarchive into"
|
||||
required: true
|
||||
type:
|
||||
description: "Compression type (tar option)"
|
||||
required: false
|
||||
default: "J"
|
||||
strip:
|
||||
description: "Strip components"
|
||||
required: false
|
||||
default: "0"
|
||||
|
||||
runs:
|
||||
using: "composite"
|
||||
steps:
|
||||
- name: Unarchive into directory
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p ${{ inputs.path }}
|
||||
cd ${{ inputs.path }}
|
||||
curl --no-progress-meter ${{ inputs.url }} | tar -${{ inputs.type }}x --strip-components=${{ inputs.strip }}
|
||||
21
.github/workflows/build.yml
vendored
21
.github/workflows/build.yml
vendored
|
|
@ -6,10 +6,24 @@ jobs:
|
|||
Build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install dependancies
|
||||
run: sudo apt update; sudo apt install -y libusb-1.0-0-dev cmake qt6-base-dev qt6-base-dev-tools qt6-tools-dev-tools libqt6widgets6 libqt6multimedia6 qt6-multimedia-dev ffmpeg wget libglx-dev libgl1-mesa-dev qt6-qpa-plugins
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Install dependancies
|
||||
run: |
|
||||
sudo add-apt-repository -y ppa:kisak/kisak-mesa
|
||||
sudo apt update
|
||||
sudo apt install -y build-essential mesa-vulkan-drivers libusb-1.0-0-dev cmake qt6-base-dev qt6-base-dev-tools qt6-tools-dev-tools libqt6widgets6 libqt6multimedia6 qt6-multimedia-dev ffmpeg wget libglx-dev libgl1-mesa-dev qt6-qpa-plugins
|
||||
- name: Get latest Vulkan SDK version
|
||||
id: vulkan_sdk_version
|
||||
run: |
|
||||
echo "VULKAN_SDK_VERSION=$(curl https://vulkan.lunarg.com/sdk/latest/linux.txt)" >> "$GITHUB_ENV"
|
||||
- name: Setup Vulkan SDK
|
||||
uses: ./.github/actions/linux-setup-vulkan
|
||||
with:
|
||||
path: ./vulkan_sdk
|
||||
version: ${{ env.VULKAN_SDK_VERSION }}
|
||||
- name: Install Appimagetool
|
||||
run: |
|
||||
wget -c https://github.com/$(wget -q https://github.com/probonopd/go-appimage/releases/expanded_assets/continuous -O - | grep "appimagetool-.*-x86_64.AppImage" | head -n 1 | cut -d '"' -f 2)
|
||||
|
|
@ -23,8 +37,9 @@ jobs:
|
|||
echo "tag=$(git describe --tags `git rev-list --tags --max-count=1`)" >> $GITHUB_OUTPUT
|
||||
- name: Build
|
||||
run: |
|
||||
source ./vulkan_sdk/setup-env.sh
|
||||
mkdir ${{ github.workspace }}/build; cd ${{ github.workspace }}/build
|
||||
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=./install/usr -DGIT_TAG=${{ steps.version.outputs.tag }} ..
|
||||
cmake -DCMAKE_BUILD_TYPE=Release -DGGML_VULKAN=ON -DCMAKE_INSTALL_PREFIX=./install/usr -DGIT_TAG=${{ steps.version.outputs.tag }} ..
|
||||
make -j $(nproc)
|
||||
make install
|
||||
- name: Create Appimage
|
||||
|
|
|
|||
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
[submodule "third_party/acestep.cpp"]
|
||||
path = third_party/acestep.cpp
|
||||
url = https://github.com/ServeurpersoCom/acestep.cpp.git
|
||||
|
|
@ -1,3 +1,6 @@
|
|||
# Copyright Carl Philipp Klemm 2026
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
cmake_minimum_required(VERSION 3.14)
|
||||
project(aceradio LANGUAGES CXX)
|
||||
|
||||
|
|
@ -7,7 +10,8 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|||
# Find Qt packages
|
||||
find_package(Qt6 COMPONENTS Core Gui Widgets Multimedia REQUIRED)
|
||||
|
||||
# Note: acestep.cpp binaries and models should be provided at runtime
|
||||
# Add acestep.cpp subdirectory
|
||||
add_subdirectory(third_party/acestep.cpp)
|
||||
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_AUTOUIC ON)
|
||||
|
|
@ -37,26 +41,29 @@ add_executable(${PROJECT_NAME}
|
|||
${MusicGeneratorGUI_H}
|
||||
res/resources.qrc
|
||||
src/elidedlabel.h src/elidedlabel.cpp
|
||||
src/SongItem.cpp
|
||||
|
||||
)
|
||||
|
||||
# UI file
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
# Link libraries (only Qt libraries - acestep.cpp is external)
|
||||
# Link libraries (Qt + acestep.cpp)
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE
|
||||
Qt6::Core
|
||||
Qt6::Gui
|
||||
Qt6::Widgets
|
||||
Qt6::Multimedia
|
||||
acestep-core
|
||||
ggml
|
||||
)
|
||||
|
||||
# Include directories (only our source directory - acestep.cpp is external)
|
||||
# Include directories
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/third_party/acestep.cpp/src
|
||||
)
|
||||
|
||||
# Note: acestep.cpp binaries (ace-qwen3, dit-vae) and models should be provided at runtime
|
||||
|
||||
# Install targets
|
||||
install(TARGETS ${PROJECT_NAME} DESTINATION bin)
|
||||
|
||||
|
|
@ -66,3 +73,24 @@ install(FILES res/xyz.uvos.aceradio.desktop DESTINATION share/applications)
|
|||
# Install icon files
|
||||
install(FILES res/xyz.uvos.aceradio.png DESTINATION share/icons/hicolor/256x256/apps RENAME xyz.uvos.aceradio.png)
|
||||
install(FILES res/xyz.uvos.aceradio.svg DESTINATION share/icons/hicolor/scalable/apps RENAME xyz.uvos.aceradio.svg)
|
||||
|
||||
# Test executable
|
||||
add_executable(test_acestep_worker
|
||||
tests/test_acestep_worker.cpp
|
||||
src/AceStepWorker.cpp
|
||||
src/AceStepWorker.h
|
||||
src/SongItem.cpp
|
||||
src/SongItem.h
|
||||
)
|
||||
|
||||
target_link_libraries(test_acestep_worker PRIVATE
|
||||
Qt6::Core
|
||||
Qt6::Widgets
|
||||
acestep-core
|
||||
ggml
|
||||
)
|
||||
|
||||
target_include_directories(test_acestep_worker PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/third_party/acestep.cpp/src
|
||||
)
|
||||
|
|
|
|||
674
LICENSE
Normal file
674
LICENSE
Normal file
|
|
@ -0,0 +1,674 @@
|
|||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
59
README.md
59
README.md
|
|
@ -3,36 +3,65 @@
|
|||

|
||||
|
||||
A C++ Qt graphical user interface for generating music using [acestep.cpp](https://github.com/ServeurpersoCom/acestep.cpp).
|
||||
Provides endless playlist style music generation on ROCm, Vulkan or CUDA.
|
||||
|
||||
## Requirements
|
||||
|
||||
- Qt 6 Core, Gui, Widgets, and Multimedia
|
||||
- CMake 3.14+
|
||||
- acestep.cpp (bring your own binaries)
|
||||
- ROCm, cuda or vulkan headers and shader compiler
|
||||
|
||||
## Installing
|
||||
|
||||
Grab the latest release from https://github.com/IMbackK/aceradio/releases/
|
||||
|
||||
## Models
|
||||
|
||||
grab the models from [here](https://huggingface.co/Serveurperso/ACE-Step-1.5-GGUF/tree/main)
|
||||
|
||||
## Building
|
||||
|
||||
### Build acestep.cpp first:
|
||||
### Linux / macOS
|
||||
|
||||
```bash
|
||||
git clone https://github.com/ServeurpersoCom/acestep.cpp.git
|
||||
cd acestep.cpp
|
||||
mkdir build && cd build
|
||||
cmake .. -DGGML_VULKAN=ON # or other backend
|
||||
make -j$(nproc)
|
||||
./models.sh # Download models (requires ~7.7 GB free space)
|
||||
```
|
||||
1. clone the repo and create a build directory
|
||||
* `git clone https://github.com/IMbackK/aceradio.git && cd aceradio`
|
||||
2. create a build directory and enter it
|
||||
* `mkdir build && cd build`
|
||||
3. configure the project
|
||||
* `cmake -DGGML_HIP=on ..` for ROCm support.
|
||||
* `cmake -DGGML_CUDA=on ..` for CUDA support.
|
||||
* `cmake -DGGML_VULKAN=on ..` for Vulkan support.
|
||||
4. build the project
|
||||
`make -j$(nproc)`
|
||||
|
||||
### Build the GUI:
|
||||
### Windows (Qt6 + CMake)
|
||||
|
||||
```bash
|
||||
To build on Windows run the following commands in *Command Prompt (cmd)* or *PowerShell*:
|
||||
|
||||
1. Configure and Build:
|
||||
```cmd
|
||||
git clone https://github.com/IMbackK/aceradio.git
|
||||
cd aceradio
|
||||
mkdir build && cd build
|
||||
cmake ..
|
||||
make -j$(nproc)
|
||||
mkdir build
|
||||
cmake -B build -DCMAKE_PREFIX_PATH="C:/Qt/6.x.x/msvc2019_64"
|
||||
|
||||
# Use --parallel to build using all CPU cores
|
||||
# Or use -j N (e.g., -j 4) to limit the number of threads
|
||||
cmake --build build --config Release --parallel
|
||||
```
|
||||
Note: Replace C:/Qt/6.x.x/msvc2019_64 with your actual Qt installation path.
|
||||
|
||||
2. Deploy Dependencies:
|
||||
Run windeployqt to copy necessary Qt libraries to the build folder:
|
||||
```cmd
|
||||
"C:\Qt\6.x.x\msvc2019_64\bin\windeployqt.exe" --no-translations build\Release\aceradio.exe
|
||||
```
|
||||
|
||||
## Setup Paths:
|
||||
|
||||
Go to settings->Ace Step->Model Paths and add the paths to the acestep.cpp binaries the models.
|
||||
Go to settings->Ace Step->Model Paths and add the paths to the AceStep models.
|
||||
|
||||
## License
|
||||
|
||||
Aceradio and all its files, unless otherwise noted, are licensed to you under the GPL-3.0-or-later license
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.5 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
|
@ -1,212 +1,397 @@
|
|||
// Copyright Carl Philipp Klemm 2026
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "AceStepWorker.h"
|
||||
#include <QFile>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QProcess>
|
||||
#include <QDir>
|
||||
#include <QStandardPaths>
|
||||
#include <QDebug>
|
||||
#include <QCoreApplication>
|
||||
#include <QRandomGenerator>
|
||||
|
||||
AceStep::AceStep(QObject* parent): QObject(parent)
|
||||
// acestep.cpp headers
|
||||
#include "pipeline-lm.h"
|
||||
#include "request.h"
|
||||
|
||||
AceStepWorker::AceStepWorker(QObject* parent)
|
||||
: QObject(parent)
|
||||
{
|
||||
connect(&qwenProcess, &QProcess::finished, this, &AceStep::qwenProcFinished);
|
||||
connect(&ditVaeProcess, &QProcess::finished, this, &AceStep::ditProcFinished);
|
||||
}
|
||||
|
||||
bool AceStep::isGenerateing(SongItem* song)
|
||||
AceStepWorker::~AceStepWorker()
|
||||
{
|
||||
if(!busy && song)
|
||||
*song = this->request.song;
|
||||
return busy;
|
||||
cancelGeneration();
|
||||
unloadModels();
|
||||
}
|
||||
|
||||
void AceStep::cancleGenerateion()
|
||||
void AceStepWorker::setModelPaths(QString lmPath, QString textEncoderPath, QString ditPath, QString vaePath)
|
||||
{
|
||||
qwenProcess.blockSignals(true);
|
||||
qwenProcess.terminate();
|
||||
qwenProcess.waitForFinished();
|
||||
qwenProcess.blockSignals(false);
|
||||
|
||||
ditVaeProcess.blockSignals(true);
|
||||
ditVaeProcess.terminate();
|
||||
ditVaeProcess.waitForFinished();
|
||||
ditVaeProcess.blockSignals(false);
|
||||
|
||||
progressUpdate(100);
|
||||
if(busy)
|
||||
generationCancled(request.song);
|
||||
|
||||
busy = false;
|
||||
// Check if paths actually changed
|
||||
bool pathsChanged = (m_lmModelPath != lmPath || m_textEncoderPath != textEncoderPath ||
|
||||
m_ditPath != ditPath || m_vaePath != vaePath);
|
||||
|
||||
m_lmModelPath = lmPath;
|
||||
m_textEncoderPath = textEncoderPath;
|
||||
m_ditPath = ditPath;
|
||||
m_vaePath = vaePath;
|
||||
|
||||
// Cache as byte arrays to avoid dangling pointers
|
||||
m_lmModelPathBytes = lmPath.toUtf8();
|
||||
m_textEncoderPathBytes = textEncoderPath.toUtf8();
|
||||
m_ditPathBytes = ditPath.toUtf8();
|
||||
m_vaePathBytes = vaePath.toUtf8();
|
||||
|
||||
// If paths changed and models are loaded, unload them so they'll be reloaded with new paths
|
||||
if (pathsChanged && m_modelsLoaded.load())
|
||||
{
|
||||
unloadModels();
|
||||
}
|
||||
}
|
||||
|
||||
bool AceStep::requestGeneration(SongItem song, QString requestTemplate, QString aceStepPath,
|
||||
QString qwen3ModelPath, QString textEncoderModelPath, QString ditModelPath,
|
||||
QString vaeModelPath)
|
||||
void AceStepWorker::setLowVramMode(bool enabled)
|
||||
{
|
||||
if(busy)
|
||||
{
|
||||
qWarning()<<"Droping song:"<<song.caption;
|
||||
return false;
|
||||
}
|
||||
busy = true;
|
||||
m_lowVramMode = enabled;
|
||||
}
|
||||
|
||||
request = {song, QRandomGenerator::global()->generate(), aceStepPath, textEncoderModelPath, ditModelPath, vaeModelPath};
|
||||
void AceStepWorker::setFlashAttention(bool enabled)
|
||||
{
|
||||
m_flashAttention = enabled;
|
||||
}
|
||||
|
||||
QString qwen3Binary = aceStepPath + "/ace-qwen3";
|
||||
QFileInfo qwen3Info(qwen3Binary);
|
||||
if (!qwen3Info.exists() || !qwen3Info.isExecutable())
|
||||
bool AceStepWorker::isGenerating(SongItem* song)
|
||||
{
|
||||
if (!m_busy.load() && song)
|
||||
*song = m_currentSong;
|
||||
return m_busy.load();
|
||||
}
|
||||
|
||||
void AceStepWorker::cancelGeneration()
|
||||
{
|
||||
m_cancelRequested.store(true);
|
||||
}
|
||||
|
||||
bool AceStepWorker::requestGeneration(SongItem song, QString requestTemplate)
|
||||
{
|
||||
if (m_busy.load())
|
||||
{
|
||||
generationError("ace-qwen3 binary not found at: " + qwen3Binary);
|
||||
busy = false;
|
||||
return false;
|
||||
}
|
||||
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;
|
||||
qWarning() << "Dropping song:" << song.caption;
|
||||
return false;
|
||||
}
|
||||
|
||||
request.requestFilePath = tempDir + "/request_" + QString::number(request.uid) + ".json";
|
||||
m_busy.store(true);
|
||||
m_cancelRequested.store(false);
|
||||
m_currentSong = song;
|
||||
m_requestTemplate = requestTemplate;
|
||||
m_uid = QRandomGenerator::global()->generate();
|
||||
|
||||
QJsonParseError parseError;
|
||||
QJsonDocument templateDoc = QJsonDocument::fromJson(requestTemplate.toUtf8(), &parseError);
|
||||
if (!templateDoc.isObject())
|
||||
// Validate model paths
|
||||
if (m_lmModelPath.isEmpty() || m_textEncoderPath.isEmpty() ||
|
||||
m_ditPath.isEmpty() || m_vaePath.isEmpty())
|
||||
{
|
||||
generationError("Invalid JSON template: " + QString(parseError.errorString()));
|
||||
busy = false;
|
||||
emit generationError("Model paths not set. Call setModelPaths() first.");
|
||||
m_busy.store(false);
|
||||
return false;
|
||||
}
|
||||
|
||||
QJsonObject requestObj = templateDoc.object();
|
||||
requestObj["caption"] = song.caption;
|
||||
|
||||
if (!song.lyrics.isEmpty())
|
||||
requestObj["lyrics"] = song.lyrics;
|
||||
if (!song.vocalLanguage.isEmpty())
|
||||
requestObj["vocal_language"] = song.vocalLanguage;
|
||||
|
||||
// Write the request file
|
||||
QFile requestFileHandle(request.requestFilePath);
|
||||
if (!requestFileHandle.open(QIODevice::WriteOnly | QIODevice::Text))
|
||||
// Validate model files exist
|
||||
if (!QFileInfo::exists(m_lmModelPath))
|
||||
{
|
||||
emit generationError("Failed to create request file: " + requestFileHandle.errorString());
|
||||
busy = false;
|
||||
emit generationError("LM model not found: " + m_lmModelPath);
|
||||
m_busy.store(false);
|
||||
return false;
|
||||
}
|
||||
if (!QFileInfo::exists(m_textEncoderPath))
|
||||
{
|
||||
emit generationError("Text encoder model not found: " + m_textEncoderPath);
|
||||
m_busy.store(false);
|
||||
return false;
|
||||
}
|
||||
if (!QFileInfo::exists(m_ditPath))
|
||||
{
|
||||
emit generationError("DiT model not found: " + m_ditPath);
|
||||
m_busy.store(false);
|
||||
return false;
|
||||
}
|
||||
if (!QFileInfo::exists(m_vaePath))
|
||||
{
|
||||
emit generationError("VAE model not found: " + m_vaePath);
|
||||
m_busy.store(false);
|
||||
return false;
|
||||
}
|
||||
requestFileHandle.write(QJsonDocument(requestObj).toJson(QJsonDocument::Indented));
|
||||
requestFileHandle.close();
|
||||
|
||||
QStringList qwen3Args;
|
||||
qwen3Args << "--request" << request.requestFilePath;
|
||||
qwen3Args << "--model" << qwen3ModelPath;
|
||||
|
||||
progressUpdate(30);
|
||||
|
||||
qwenProcess.start(qwen3Binary, qwen3Args);
|
||||
// Run generation in the worker thread
|
||||
QMetaObject::invokeMethod(this, "runGeneration", Qt::QueuedConnection);
|
||||
return true;
|
||||
}
|
||||
|
||||
void AceStep::qwenProcFinished(int code, QProcess::ExitStatus status)
|
||||
bool AceStepWorker::checkCancel(void* data)
|
||||
{
|
||||
QFile::remove(request.requestFilePath);
|
||||
if(code != 0)
|
||||
AceStepWorker* worker = static_cast<AceStepWorker*>(data);
|
||||
return worker->m_cancelRequested.load();
|
||||
}
|
||||
|
||||
std::shared_ptr<QByteArray> AceStepWorker::convertToWav(const AceAudio& audio)
|
||||
{
|
||||
auto audioData = std::make_shared<QByteArray>();
|
||||
|
||||
// Simple WAV header + stereo float data
|
||||
int numChannels = 2;
|
||||
int bitsPerSample = 16;
|
||||
int byteRate = audio.sample_rate * numChannels * (bitsPerSample / 8);
|
||||
int blockAlign = numChannels * (bitsPerSample / 8);
|
||||
int dataSize = audio.n_samples * numChannels * (bitsPerSample / 8);
|
||||
|
||||
// RIFF header
|
||||
audioData->append("RIFF");
|
||||
audioData->append(QByteArray::fromRawData(reinterpret_cast<const char*>(&dataSize), 4));
|
||||
audioData->append("WAVE");
|
||||
|
||||
// fmt chunk
|
||||
audioData->append("fmt ");
|
||||
int fmtSize = 16;
|
||||
audioData->append(QByteArray::fromRawData(reinterpret_cast<const char*>(&fmtSize), 4));
|
||||
short audioFormat = 1; // PCM
|
||||
audioData->append(QByteArray::fromRawData(reinterpret_cast<const char*>(&audioFormat), 2));
|
||||
short numCh = numChannels;
|
||||
audioData->append(QByteArray::fromRawData(reinterpret_cast<const char*>(&numCh), 2));
|
||||
int sampleRate = audio.sample_rate;
|
||||
audioData->append(QByteArray::fromRawData(reinterpret_cast<const char*>(&sampleRate), 4));
|
||||
audioData->append(QByteArray::fromRawData(reinterpret_cast<const char*>(&byteRate), 4));
|
||||
audioData->append(QByteArray::fromRawData(reinterpret_cast<const char*>(&blockAlign), 2));
|
||||
audioData->append(QByteArray::fromRawData(reinterpret_cast<const char*>(&bitsPerSample), 2));
|
||||
|
||||
// data chunk
|
||||
audioData->append("data");
|
||||
audioData->append(QByteArray::fromRawData(reinterpret_cast<const char*>(&dataSize), 4));
|
||||
|
||||
// Convert float samples to 16-bit and write
|
||||
QVector<short> interleaved(audio.n_samples * numChannels);
|
||||
for (int i = 0; i < audio.n_samples; i++)
|
||||
{
|
||||
QString errorOutput = qwenProcess.readAllStandardError();
|
||||
generationError("dit-vae exited with code " + QString::number(code) + ": " + errorOutput);
|
||||
busy = false;
|
||||
float left = audio.samples[i];
|
||||
float right = audio.samples[i + audio.n_samples];
|
||||
// Clamp and convert to 16-bit
|
||||
left = std::max(-1.0f, std::min(1.0f, left));
|
||||
right = std::max(-1.0f, std::min(1.0f, right));
|
||||
interleaved[i * 2] = static_cast<short>(left * 32767.0f);
|
||||
interleaved[i * 2 + 1] = static_cast<short>(right * 32767.0f);
|
||||
}
|
||||
audioData->append(QByteArray::fromRawData(reinterpret_cast<const char*>(interleaved.data()), dataSize));
|
||||
return audioData;
|
||||
}
|
||||
|
||||
void AceStepWorker::runGeneration()
|
||||
{
|
||||
// Convert SongItem to AceRequest
|
||||
AceRequest req = songToRequest(m_currentSong, m_requestTemplate);
|
||||
AceRequest lmOutput;
|
||||
request_init(&lmOutput);
|
||||
|
||||
emit progressUpdate(10);
|
||||
|
||||
if (!loadLm())
|
||||
{
|
||||
m_busy.store(false);
|
||||
return;
|
||||
}
|
||||
|
||||
QString ditVaeBinary = request.aceStepPath + "/dit-vae";
|
||||
QFileInfo ditVaeInfo(ditVaeBinary);
|
||||
if (!ditVaeInfo.exists() || !ditVaeInfo.isExecutable())
|
||||
emit progressUpdate(30);
|
||||
|
||||
int lmResult = ace_lm_generate(m_lmContext, &req, 1, &lmOutput,
|
||||
nullptr, nullptr,
|
||||
checkCancel, this,
|
||||
LM_MODE_GENERATE);
|
||||
|
||||
if (m_cancelRequested.load())
|
||||
{
|
||||
generationError("dit-vae binary not found at: " + ditVaeBinary);
|
||||
busy = false;
|
||||
if(m_lowVramMode)
|
||||
unloadModels();
|
||||
emit generationCanceled(m_currentSong);
|
||||
m_busy.store(false);
|
||||
return;
|
||||
}
|
||||
|
||||
request.requestLlmFilePath = tempDir + "/request_" + QString::number(request.uid) + "0.json";
|
||||
if (!QFileInfo::exists(request.requestLlmFilePath))
|
||||
if (lmResult != 0)
|
||||
{
|
||||
generationError("ace-qwen3 failed to create enhaced request file "+request.requestLlmFilePath);
|
||||
busy = false;
|
||||
if(m_lowVramMode)
|
||||
unloadModels();
|
||||
emit generationError("LM generation failed or was canceled");
|
||||
m_busy.store(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// Load lyrics from the enhanced request file
|
||||
QFile lmOutputFile(request.requestLlmFilePath);
|
||||
if (lmOutputFile.open(QIODevice::ReadOnly | QIODevice::Text))
|
||||
m_currentSong.lyrics = QString::fromStdString(lmOutput.lyrics);
|
||||
|
||||
if(m_lowVramMode)
|
||||
unloadLm();
|
||||
|
||||
emit progressUpdate(50);
|
||||
|
||||
if (!loadSynth())
|
||||
{
|
||||
m_busy.store(false);
|
||||
return;
|
||||
}
|
||||
|
||||
emit progressUpdate(60);
|
||||
|
||||
AceAudio outputAudio;
|
||||
outputAudio.samples = nullptr;
|
||||
outputAudio.n_samples = 0;
|
||||
outputAudio.sample_rate = 48000;
|
||||
|
||||
int synthResult = ace_synth_generate(m_synthContext, &lmOutput,
|
||||
nullptr, 0, // no source audio
|
||||
nullptr, 0, // no reference audio
|
||||
1, &outputAudio,
|
||||
checkCancel, this);
|
||||
|
||||
if(m_lowVramMode)
|
||||
unloadSynth();
|
||||
|
||||
if (m_cancelRequested.load())
|
||||
{
|
||||
emit generationCanceled(m_currentSong);
|
||||
m_busy.store(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (synthResult != 0)
|
||||
{
|
||||
emit generationError("Synthesis failed or was canceled");
|
||||
m_busy.store(false);
|
||||
return;
|
||||
}
|
||||
|
||||
std::shared_ptr<QByteArray> audioData = convertToWav(outputAudio);
|
||||
ace_audio_free(&outputAudio);
|
||||
|
||||
m_currentSong.json = QString::fromStdString(request_to_json(&lmOutput, true));
|
||||
m_currentSong.audioData = audioData;
|
||||
|
||||
if (lmOutput.bpm > 0)
|
||||
m_currentSong.bpm = lmOutput.bpm;
|
||||
|
||||
if (!lmOutput.keyscale.empty())
|
||||
m_currentSong.key = QString::fromStdString(lmOutput.keyscale);
|
||||
|
||||
emit progressUpdate(100);
|
||||
emit songGenerated(m_currentSong);
|
||||
|
||||
m_busy.store(false);
|
||||
}
|
||||
|
||||
bool AceStepWorker::loadModels()
|
||||
{
|
||||
bool ret = loadSynth();
|
||||
if(!ret)
|
||||
return false;
|
||||
|
||||
ret = loadLm();
|
||||
if(!ret)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void AceStepWorker::unloadModels()
|
||||
{
|
||||
if (m_synthContext)
|
||||
{
|
||||
ace_synth_free(m_synthContext);
|
||||
m_synthContext = nullptr;
|
||||
}
|
||||
if (m_lmContext)
|
||||
{
|
||||
ace_lm_free(m_lmContext);
|
||||
m_lmContext = nullptr;
|
||||
}
|
||||
m_modelsLoaded.store(false);
|
||||
}
|
||||
|
||||
bool AceStepWorker::loadLm()
|
||||
{
|
||||
if (m_lmContext)
|
||||
return true;
|
||||
|
||||
AceLmParams lmParams;
|
||||
ace_lm_default_params(&lmParams);
|
||||
lmParams.model_path = m_lmModelPathBytes.constData();
|
||||
lmParams.use_fsm = true;
|
||||
lmParams.use_fa = m_flashAttention;
|
||||
|
||||
m_lmContext = ace_lm_load(&lmParams);
|
||||
if (!m_lmContext)
|
||||
{
|
||||
emit generationError("Failed to load LM model: " + m_lmModelPath);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void AceStepWorker::unloadLm()
|
||||
{
|
||||
if (m_lmContext)
|
||||
{
|
||||
ace_lm_free(m_lmContext);
|
||||
m_lmContext = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool AceStepWorker::loadSynth()
|
||||
{
|
||||
if (m_synthContext)
|
||||
return true;
|
||||
|
||||
AceSynthParams synthParams;
|
||||
ace_synth_default_params(&synthParams);
|
||||
synthParams.text_encoder_path = m_textEncoderPathBytes.constData();
|
||||
synthParams.dit_path = m_ditPathBytes.constData();
|
||||
synthParams.vae_path = m_vaePathBytes.constData();
|
||||
synthParams.use_fa = m_flashAttention;
|
||||
|
||||
m_synthContext = ace_synth_load(&synthParams);
|
||||
if (!m_synthContext)
|
||||
{
|
||||
emit generationError("Failed to load synthesis models");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void AceStepWorker::unloadSynth()
|
||||
{
|
||||
if (m_synthContext)
|
||||
{
|
||||
ace_synth_free(m_synthContext);
|
||||
m_synthContext = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
AceRequest AceStepWorker::songToRequest(const SongItem& song, const QString& templateJson)
|
||||
{
|
||||
AceRequest req;
|
||||
request_init(&req);
|
||||
|
||||
// Parse template first to get all default values
|
||||
QJsonObject requestJson;
|
||||
if (!templateJson.isEmpty())
|
||||
{
|
||||
QJsonParseError parseError;
|
||||
request.song.json = lmOutputFile.readAll();
|
||||
QJsonDocument doc = QJsonDocument::fromJson(request.song.json.toUtf8(), &parseError);
|
||||
lmOutputFile.close();
|
||||
|
||||
if (doc.isObject() && !parseError.error)
|
||||
{
|
||||
QJsonObject obj = doc.object();
|
||||
if (obj.contains("lyrics") && obj["lyrics"].isString())
|
||||
request.song.lyrics = obj["lyrics"].toString();
|
||||
}
|
||||
QJsonDocument templateDoc = QJsonDocument::fromJson(templateJson.toUtf8(), &parseError);
|
||||
if (templateDoc.isObject())
|
||||
requestJson = templateDoc.object();
|
||||
else
|
||||
qWarning() << "Failed to parse request template:" << parseError.errorString();
|
||||
}
|
||||
|
||||
// Step 2: Run dit-vae to generate audio
|
||||
QStringList ditVaeArgs;
|
||||
ditVaeArgs << "--request"<<request.requestLlmFilePath;
|
||||
ditVaeArgs << "--text-encoder"<<request.textEncoderModelPath;
|
||||
ditVaeArgs << "--dit"<<request.ditModelPath;
|
||||
ditVaeArgs << "--vae"<<request.vaeModelPath;
|
||||
song.store(requestJson);
|
||||
QJsonDocument requestJsonDoc(requestJson);
|
||||
QString requestJsonString = QString::fromUtf8(requestJsonDoc.toJson(QJsonDocument::Compact));
|
||||
if (!request_parse_json(&req, requestJsonString.toUtf8()))
|
||||
qWarning() << "Failed to parse merged request JSON";
|
||||
|
||||
progressUpdate(60);
|
||||
|
||||
ditVaeProcess.start(ditVaeBinary, ditVaeArgs);
|
||||
}
|
||||
|
||||
void AceStep::ditProcFinished(int code, QProcess::ExitStatus status)
|
||||
{
|
||||
QFile::remove(request.requestLlmFilePath);
|
||||
if (code != 0)
|
||||
{
|
||||
QString errorOutput = ditVaeProcess.readAllStandardError();
|
||||
generationError("dit-vae exited with code " + QString::number(code) + ": " + errorOutput);
|
||||
busy = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the generated WAV file
|
||||
QString wavFile = tempDir+"/request_" + QString::number(request.uid) + "00.wav";
|
||||
if (!QFileInfo::exists(wavFile))
|
||||
{
|
||||
generationError("No WAV file generated at "+wavFile);
|
||||
busy = false;
|
||||
return;
|
||||
}
|
||||
busy = false;
|
||||
|
||||
progressUpdate(100);
|
||||
request.song.file = wavFile;
|
||||
songGenerated(request.song);
|
||||
}
|
||||
if (req.seed < 0)
|
||||
req.seed = static_cast<int64_t>(QRandomGenerator::global()->generate());
|
||||
|
||||
return req;
|
||||
}
|
||||
|
|
@ -1,56 +1,98 @@
|
|||
/*
|
||||
* Copyright Carl Philipp Klemm 2026
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef ACESTEPWORKER_H
|
||||
#define ACESTEPWORKER_H
|
||||
|
||||
#include <QObject>
|
||||
#include <QString>
|
||||
#include <QProcess>
|
||||
#include <QThread>
|
||||
#include <QStandardPaths>
|
||||
#include <atomic>
|
||||
|
||||
#include "SongItem.h"
|
||||
#include "pipeline-synth.h"
|
||||
#include "request.h"
|
||||
|
||||
class AceStep : public QObject
|
||||
struct AceLm;
|
||||
struct AceSynth;
|
||||
|
||||
class AceStepWorker : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
QProcess qwenProcess;
|
||||
QProcess ditVaeProcess;
|
||||
|
||||
bool busy = false;
|
||||
public:
|
||||
explicit AceStepWorker(QObject* parent = nullptr);
|
||||
~AceStepWorker();
|
||||
|
||||
struct Request
|
||||
{
|
||||
SongItem song;
|
||||
uint64_t uid;
|
||||
QString aceStepPath;
|
||||
QString textEncoderModelPath;
|
||||
QString ditModelPath;
|
||||
QString vaeModelPath;
|
||||
QString requestFilePath;
|
||||
QString requestLlmFilePath;
|
||||
};
|
||||
bool isGenerating(SongItem* song = nullptr);
|
||||
void cancelGeneration();
|
||||
|
||||
Request request;
|
||||
// Model paths - set these before first generation
|
||||
void setModelPaths(QString lmPath, QString textEncoderPath, QString ditPath, QString vaePath);
|
||||
|
||||
const QString tempDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation);
|
||||
// Low VRAM mode: unload models between phases to save VRAM
|
||||
void setLowVramMode(bool enabled);
|
||||
bool isLowVramMode() const { return m_lowVramMode; }
|
||||
|
||||
// Flash attention mode
|
||||
void setFlashAttention(bool enabled);
|
||||
bool isFlashAttention() const { return m_flashAttention; }
|
||||
|
||||
// Request a new song generation
|
||||
bool requestGeneration(SongItem song, QString requestTemplate);
|
||||
|
||||
signals:
|
||||
void songGenerated(SongItem song);
|
||||
void generationCancled(SongItem song);
|
||||
void generationCanceled(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);
|
||||
void runGeneration();
|
||||
|
||||
private:
|
||||
static bool checkCancel(void* data);
|
||||
|
||||
bool loadModels();
|
||||
void unloadModels();
|
||||
bool loadLm();
|
||||
void unloadLm();
|
||||
bool loadSynth();
|
||||
void unloadSynth();
|
||||
|
||||
AceRequest songToRequest(const SongItem& song, const QString& templateJson);
|
||||
|
||||
static std::shared_ptr<QByteArray> convertToWav(const AceAudio& audio);
|
||||
|
||||
// Generation state
|
||||
std::atomic<bool> m_busy{false};
|
||||
std::atomic<bool> m_cancelRequested{false};
|
||||
std::atomic<bool> m_modelsLoaded{false};
|
||||
bool m_lowVramMode = false;
|
||||
bool m_flashAttention = true;
|
||||
|
||||
// Current request data
|
||||
SongItem m_currentSong;
|
||||
QString m_requestTemplate;
|
||||
uint64_t m_uid;
|
||||
|
||||
// Model paths
|
||||
QString m_lmModelPath;
|
||||
QString m_textEncoderPath;
|
||||
QString m_ditPath;
|
||||
QString m_vaePath;
|
||||
|
||||
// Loaded models (accessed from worker thread only)
|
||||
AceLm* m_lmContext = nullptr;
|
||||
AceSynth* m_synthContext = nullptr;
|
||||
|
||||
QByteArray m_lmModelPathBytes;
|
||||
QByteArray m_textEncoderPathBytes;
|
||||
QByteArray m_ditPathBytes;
|
||||
QByteArray m_vaePathBytes;
|
||||
};
|
||||
|
||||
#endif // ACESTEPWORKER_H
|
||||
#endif // ACESTEPWORKER_H
|
||||
|
|
@ -1,3 +1,6 @@
|
|||
// Copyright Carl Philipp Klemm 2026
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "AdvancedSettingsDialog.h"
|
||||
#include "ui_AdvancedSettingsDialog.h"
|
||||
#include <QFileDialog>
|
||||
|
|
@ -10,6 +13,12 @@ AdvancedSettingsDialog::AdvancedSettingsDialog(QWidget *parent)
|
|||
ui(new Ui::AdvancedSettingsDialog)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
// Connect signals and slots explicitly
|
||||
connect(ui->qwen3BrowseButton, &QPushButton::clicked, this, &AdvancedSettingsDialog::onQwen3BrowseButtonClicked);
|
||||
connect(ui->textEncoderBrowseButton, &QPushButton::clicked, this, &AdvancedSettingsDialog::onTextEncoderBrowseButtonClicked);
|
||||
connect(ui->ditBrowseButton, &QPushButton::clicked, this, &AdvancedSettingsDialog::onDiTBrowseButtonClicked);
|
||||
connect(ui->vaeBrowseButton, &QPushButton::clicked, this, &AdvancedSettingsDialog::onVAEBrowseButtonClicked);
|
||||
}
|
||||
|
||||
AdvancedSettingsDialog::~AdvancedSettingsDialog()
|
||||
|
|
@ -22,11 +31,6 @@ QString AdvancedSettingsDialog::getJsonTemplate() const
|
|||
return ui->jsonTemplateEdit->toPlainText();
|
||||
}
|
||||
|
||||
QString AdvancedSettingsDialog::getAceStepPath() const
|
||||
{
|
||||
return ui->aceStepPathEdit->text();
|
||||
}
|
||||
|
||||
QString AdvancedSettingsDialog::getQwen3ModelPath() const
|
||||
{
|
||||
return ui->qwen3ModelEdit->text();
|
||||
|
|
@ -47,16 +51,21 @@ QString AdvancedSettingsDialog::getVAEModelPath() const
|
|||
return ui->vaeModelEdit->text();
|
||||
}
|
||||
|
||||
bool AdvancedSettingsDialog::getLowVramMode() const
|
||||
{
|
||||
return ui->lowVramCheckBox->isChecked();
|
||||
}
|
||||
|
||||
bool AdvancedSettingsDialog::getFlashAttention() const
|
||||
{
|
||||
return ui->flashAttentionCheckBox->isChecked();
|
||||
}
|
||||
|
||||
void AdvancedSettingsDialog::setJsonTemplate(const QString &templateStr)
|
||||
{
|
||||
ui->jsonTemplateEdit->setPlainText(templateStr);
|
||||
}
|
||||
|
||||
void AdvancedSettingsDialog::setAceStepPath(const QString &path)
|
||||
{
|
||||
ui->aceStepPathEdit->setText(path);
|
||||
}
|
||||
|
||||
void AdvancedSettingsDialog::setQwen3ModelPath(const QString &path)
|
||||
{
|
||||
ui->qwen3ModelEdit->setText(path);
|
||||
|
|
@ -77,16 +86,17 @@ void AdvancedSettingsDialog::setVAEModelPath(const QString &path)
|
|||
ui->vaeModelEdit->setText(path);
|
||||
}
|
||||
|
||||
void AdvancedSettingsDialog::on_aceStepBrowseButton_clicked()
|
||||
void AdvancedSettingsDialog::setLowVramMode(bool enabled)
|
||||
{
|
||||
QString dir = QFileDialog::getExistingDirectory(this, "Select AceStep Build Directory", ui->aceStepPathEdit->text());
|
||||
if (!dir.isEmpty())
|
||||
{
|
||||
ui->aceStepPathEdit->setText(dir);
|
||||
}
|
||||
ui->lowVramCheckBox->setChecked(enabled);
|
||||
}
|
||||
|
||||
void AdvancedSettingsDialog::on_qwen3BrowseButton_clicked()
|
||||
void AdvancedSettingsDialog::setFlashAttention(bool enabled)
|
||||
{
|
||||
ui->flashAttentionCheckBox->setChecked(enabled);
|
||||
}
|
||||
|
||||
void AdvancedSettingsDialog::onQwen3BrowseButtonClicked()
|
||||
{
|
||||
QString file = QFileDialog::getOpenFileName(this, "Select Qwen3 Model", ui->qwen3ModelEdit->text(),
|
||||
"GGUF Files (*.gguf)");
|
||||
|
|
@ -96,7 +106,7 @@ void AdvancedSettingsDialog::on_qwen3BrowseButton_clicked()
|
|||
}
|
||||
}
|
||||
|
||||
void AdvancedSettingsDialog::on_textEncoderBrowseButton_clicked()
|
||||
void AdvancedSettingsDialog::onTextEncoderBrowseButtonClicked()
|
||||
{
|
||||
QString file = QFileDialog::getOpenFileName(this, "Select Text Encoder Model", ui->textEncoderEdit->text(),
|
||||
"GGUF Files (*.gguf)");
|
||||
|
|
@ -106,7 +116,7 @@ void AdvancedSettingsDialog::on_textEncoderBrowseButton_clicked()
|
|||
}
|
||||
}
|
||||
|
||||
void AdvancedSettingsDialog::on_ditBrowseButton_clicked()
|
||||
void AdvancedSettingsDialog::onDiTBrowseButtonClicked()
|
||||
{
|
||||
QString file = QFileDialog::getOpenFileName(this, "Select DiT Model", ui->ditModelEdit->text(), "GGUF Files (*.gguf)");
|
||||
if (!file.isEmpty())
|
||||
|
|
@ -115,7 +125,7 @@ void AdvancedSettingsDialog::on_ditBrowseButton_clicked()
|
|||
}
|
||||
}
|
||||
|
||||
void AdvancedSettingsDialog::on_vaeBrowseButton_clicked()
|
||||
void AdvancedSettingsDialog::onVAEBrowseButtonClicked()
|
||||
{
|
||||
QString file = QFileDialog::getOpenFileName(this, "Select VAE Model", ui->vaeModelEdit->text(), "GGUF Files (*.gguf)");
|
||||
if (!file.isEmpty())
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
/*
|
||||
* Copyright Carl Philipp Klemm 2026
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef ADVANCEDSETTINGSDIALOG_H
|
||||
#define ADVANCEDSETTINGSDIALOG_H
|
||||
|
||||
|
|
@ -19,26 +24,27 @@ public:
|
|||
|
||||
// Getters for settings
|
||||
QString getJsonTemplate() const;
|
||||
QString getAceStepPath() const;
|
||||
QString getQwen3ModelPath() const;
|
||||
QString getTextEncoderModelPath() const;
|
||||
QString getDiTModelPath() const;
|
||||
QString getVAEModelPath() const;
|
||||
bool getLowVramMode() const;
|
||||
bool getFlashAttention() const;
|
||||
|
||||
// Setters for settings
|
||||
void setJsonTemplate(const QString &templateStr);
|
||||
void setAceStepPath(const QString &path);
|
||||
void setQwen3ModelPath(const QString &path);
|
||||
void setTextEncoderModelPath(const QString &path);
|
||||
void setDiTModelPath(const QString &path);
|
||||
void setVAEModelPath(const QString &path);
|
||||
void setLowVramMode(bool enabled);
|
||||
void setFlashAttention(bool enabled);
|
||||
|
||||
private slots:
|
||||
void on_aceStepBrowseButton_clicked();
|
||||
void on_qwen3BrowseButton_clicked();
|
||||
void on_textEncoderBrowseButton_clicked();
|
||||
void on_ditBrowseButton_clicked();
|
||||
void on_vaeBrowseButton_clicked();
|
||||
void onQwen3BrowseButtonClicked();
|
||||
void onTextEncoderBrowseButtonClicked();
|
||||
void onDiTBrowseButtonClicked();
|
||||
void onVAEBrowseButtonClicked();
|
||||
|
||||
private:
|
||||
Ui::AdvancedSettingsDialog *ui;
|
||||
|
|
|
|||
|
|
@ -1,203 +1,239 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>AdvancedSettingsDialog</class>
|
||||
<widget class="QDialog" name="AdvancedSettingsDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>600</width>
|
||||
<height>450</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Advanced Settings</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QTabWidget" name="tabWidget">
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="jsonTab">
|
||||
<attribute name="title">
|
||||
<string>JSON Template</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="jsonLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="jsonLabel">
|
||||
<property name="text">
|
||||
<string>JSON Template for AceStep generation:</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextEdit" name="jsonTemplateEdit"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="pathsTab">
|
||||
<attribute name="title">
|
||||
<string>Model Paths</string>
|
||||
</attribute>
|
||||
<layout class="QFormLayout" name="pathsLayout">
|
||||
<property name="fieldGrowthPolicy">
|
||||
<enum>QFormLayout::FieldGrowthPolicy::AllNonFixedFieldsGrow</enum>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="aceStepLabel">
|
||||
<property name="text">
|
||||
<string>AceStep Path:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<layout class="QHBoxLayout" name="aceStepLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="aceStepPathEdit"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="aceStepBrowseButton">
|
||||
<property name="text">
|
||||
<string>Browse...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="qwen3Label">
|
||||
<property name="text">
|
||||
<string>Qwen3 Model:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<layout class="QHBoxLayout" name="qwen3Layout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="qwen3ModelEdit"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="qwen3BrowseButton">
|
||||
<property name="text">
|
||||
<string>Browse...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="textEncoderLabel">
|
||||
<property name="text">
|
||||
<string>Text Encoder Model:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<layout class="QHBoxLayout" name="textEncoderLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="textEncoderEdit"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="textEncoderBrowseButton">
|
||||
<property name="text">
|
||||
<string>Browse...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="ditLabel">
|
||||
<property name="text">
|
||||
<string>DiT Model:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<layout class="QHBoxLayout" name="ditLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="ditModelEdit"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="ditBrowseButton">
|
||||
<property name="text">
|
||||
<string>Browse...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<widget class="QLabel" name="vaeLabel">
|
||||
<property name="text">
|
||||
<string>VAE Model:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="1">
|
||||
<layout class="QHBoxLayout" name="vaeLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="vaeModelEdit"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="vaeBrowseButton">
|
||||
<property name="text">
|
||||
<string>Browse...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Save</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>AdvancedSettingsDialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>AdvancedSettingsDialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
<ui version="4.0">
|
||||
<class>AdvancedSettingsDialog</class>
|
||||
<widget class="QDialog" name="AdvancedSettingsDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>600</width>
|
||||
<height>450</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Advanced Settings</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QTabWidget" name="tabWidget">
|
||||
<property name="currentIndex">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<widget class="QWidget" name="performanceTab">
|
||||
<attribute name="title">
|
||||
<string>Performance</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="performanceLayout">
|
||||
<item>
|
||||
<widget class="QCheckBox" name="lowVramCheckBox">
|
||||
<property name="text">
|
||||
<string>Low VRAM Mode</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="lowVramLabel">
|
||||
<property name="text">
|
||||
<string>Unload models between generation phases to save VRAM. Slower but uses less memory.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="flashAttentionCheckBox">
|
||||
<property name="text">
|
||||
<string>Flash Attention</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="flashAttentionLabel">
|
||||
<property name="text">
|
||||
<string>Use flash attention for faster generation. Disable if experiencing poor output quality on vulkan.</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="verticalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Vertical</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>20</width>
|
||||
<height>40</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="jsonTab">
|
||||
<attribute name="title">
|
||||
<string>JSON Template</string>
|
||||
</attribute>
|
||||
<layout class="QVBoxLayout" name="jsonLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="jsonLabel">
|
||||
<property name="text">
|
||||
<string>JSON Template for AceStep generation:</string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextEdit" name="jsonTemplateEdit"/>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<widget class="QWidget" name="pathsTab">
|
||||
<attribute name="title">
|
||||
<string>Model Paths</string>
|
||||
</attribute>
|
||||
<layout class="QFormLayout" name="pathsLayout">
|
||||
<property name="fieldGrowthPolicy">
|
||||
<enum>QFormLayout::FieldGrowthPolicy::AllNonFixedFieldsGrow</enum>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="qwen3Label">
|
||||
<property name="text">
|
||||
<string>Qwen3 Model:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<layout class="QHBoxLayout" name="qwen3Layout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="qwen3ModelEdit"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="qwen3BrowseButton">
|
||||
<property name="text">
|
||||
<string>Browse...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="textEncoderLabel">
|
||||
<property name="text">
|
||||
<string>Text Encoder Model:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<layout class="QHBoxLayout" name="textEncoderLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="textEncoderEdit"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="textEncoderBrowseButton">
|
||||
<property name="text">
|
||||
<string>Browse...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="ditLabel">
|
||||
<property name="text">
|
||||
<string>DiT Model:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<layout class="QHBoxLayout" name="ditLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="ditModelEdit"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="ditBrowseButton">
|
||||
<property name="text">
|
||||
<string>Browse...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="vaeLabel">
|
||||
<property name="text">
|
||||
<string>VAE Model:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="1">
|
||||
<layout class="QHBoxLayout" name="vaeLayout">
|
||||
<item>
|
||||
<widget class="QLineEdit" name="vaeModelEdit"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="vaeBrowseButton">
|
||||
<property name="text">
|
||||
<string>Browse...</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="buttonBox">
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Save</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>AdvancedSettingsDialog</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>buttonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>AdvancedSettingsDialog</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
// Copyright Carl Philipp Klemm 2026
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "AudioPlayer.h"
|
||||
#include <QBuffer>
|
||||
#include <QDebug>
|
||||
|
||||
AudioPlayer::AudioPlayer(QObject *parent)
|
||||
|
|
@ -45,6 +49,33 @@ void AudioPlayer::play(const QString &filePath)
|
|||
positionTimer->start();
|
||||
}
|
||||
|
||||
void AudioPlayer::play(std::shared_ptr<QByteArray> audioData)
|
||||
{
|
||||
if (isPlaying())
|
||||
{
|
||||
stop();
|
||||
}
|
||||
|
||||
if (!audioData || audioData->isEmpty())
|
||||
{
|
||||
emit playbackError("No audio data available");
|
||||
return;
|
||||
}
|
||||
|
||||
// Create a buffer with the audio data
|
||||
QBuffer *buffer = new QBuffer();
|
||||
buffer->setData(*audioData);
|
||||
buffer->open(QIODevice::ReadOnly);
|
||||
buffer->setParent(this);
|
||||
|
||||
// Use QMediaPlayer::setSourceDevice for in-memory playback
|
||||
mediaPlayer->setSourceDevice(buffer);
|
||||
mediaPlayer->play();
|
||||
|
||||
// Start position timer
|
||||
positionTimer->start();
|
||||
}
|
||||
|
||||
void AudioPlayer::play()
|
||||
{
|
||||
if (!isPlaying())
|
||||
|
|
@ -89,6 +120,19 @@ int AudioPlayer::position() const
|
|||
return mediaPlayer->position();
|
||||
}
|
||||
|
||||
void AudioPlayer::setVolume(int volume)
|
||||
{
|
||||
// Convert from 0-100 range to 0.0-1.0 range
|
||||
qreal volumeLevel = static_cast<qreal>(volume) / 100.0;
|
||||
audioOutput->setVolume(volumeLevel);
|
||||
}
|
||||
|
||||
int AudioPlayer::getVolume() const
|
||||
{
|
||||
// Convert from 0.0-1.0 range to 0-100 range
|
||||
return static_cast<int>(audioOutput->volume() * 100);
|
||||
}
|
||||
|
||||
void AudioPlayer::handlePlaybackStateChanged(QMediaPlayer::PlaybackState state)
|
||||
{
|
||||
if (state == QMediaPlayer::PlayingState)
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
/*
|
||||
* Copyright Carl Philipp Klemm 2026
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef AUDIOPLAYER_H
|
||||
#define AUDIOPLAYER_H
|
||||
|
||||
|
|
@ -9,6 +14,7 @@
|
|||
#include <QMediaDevices>
|
||||
#include <QAudioDevice>
|
||||
#include <QTimer>
|
||||
#include <memory>
|
||||
|
||||
class AudioPlayer : public QObject
|
||||
{
|
||||
|
|
@ -18,6 +24,7 @@ public:
|
|||
~AudioPlayer();
|
||||
|
||||
void play(const QString &filePath);
|
||||
void play(std::shared_ptr<QByteArray> audioData);
|
||||
void play();
|
||||
void stop();
|
||||
void pause();
|
||||
|
|
@ -25,6 +32,8 @@ public:
|
|||
bool isPlaying() const;
|
||||
int duration() const;
|
||||
int position() const;
|
||||
void setVolume(int volume);
|
||||
int getVolume() const;
|
||||
|
||||
signals:
|
||||
void playbackStarted();
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
// Copyright Carl Philipp Klemm 2026
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "MainWindow.h"
|
||||
#include "ui_MainWindow.h"
|
||||
#include "SongDialog.h"
|
||||
|
|
@ -18,7 +21,7 @@ MainWindow::MainWindow(QWidget *parent)
|
|||
ui(new Ui::MainWindow),
|
||||
songModel(new SongListModel(this)),
|
||||
audioPlayer(new AudioPlayer(this)),
|
||||
aceStep(new AceStep(this)),
|
||||
aceStep(new AceStepWorker),
|
||||
playbackTimer(new QTimer(this)),
|
||||
isPlaying(false),
|
||||
isPaused(false),
|
||||
|
|
@ -26,6 +29,7 @@ MainWindow::MainWindow(QWidget *parent)
|
|||
isGeneratingNext(false)
|
||||
{
|
||||
aceStep->moveToThread(&aceThread);
|
||||
aceThread.setObjectName("AceStep Woker Thread");
|
||||
|
||||
ui->setupUi(this);
|
||||
|
||||
|
|
@ -38,15 +42,27 @@ MainWindow::MainWindow(QWidget *parent)
|
|||
// Load settings
|
||||
loadSettings();
|
||||
|
||||
// Set model paths for acestep.cpp
|
||||
aceStep->setModelPaths(qwen3ModelPath, textEncoderModelPath, ditModelPath, vaeModelPath);
|
||||
|
||||
// Auto-load playlist from config location on startup
|
||||
autoLoadPlaylist();
|
||||
|
||||
// Connect signals and slots
|
||||
connect(ui->actionAdvancedSettings, &QAction::triggered, this, &MainWindow::on_advancedSettingsButton_clicked);
|
||||
connect(ui->actionSavePlaylist, &QAction::triggered, this, &MainWindow::on_actionSavePlaylist);
|
||||
connect(ui->actionLoadPlaylist, &QAction::triggered, this, &MainWindow::on_actionLoadPlaylist);
|
||||
connect(ui->actionAppendPlaylist, &QAction::triggered, this, &MainWindow::on_actionAppendPlaylist);
|
||||
connect(ui->actionSaveSong, &QAction::triggered, this, &MainWindow::on_actionSaveSong);
|
||||
connect(ui->playButton, &QPushButton::clicked, this, &MainWindow::onPlayButtonClicked);
|
||||
connect(ui->pauseButton, &QPushButton::clicked, this, &MainWindow::onPauseButtonClicked);
|
||||
connect(ui->skipButton, &QPushButton::clicked, this, &MainWindow::onSkipButtonClicked);
|
||||
connect(ui->stopButton, &QPushButton::clicked, this, &MainWindow::onStopButtonClicked);
|
||||
connect(ui->shuffleButton, &QPushButton::clicked, this, &MainWindow::onShuffleButtonClicked);
|
||||
connect(ui->volumeSlider, &QSlider::valueChanged, this, &MainWindow::onVolumeSliderValueChanged);
|
||||
connect(ui->addSongButton, &QPushButton::clicked, this, &MainWindow::onAddSongButtonClicked);
|
||||
connect(ui->removeSongButton, &QPushButton::clicked, this, &MainWindow::onRemoveSongButtonClicked);
|
||||
connect(ui->actionAdvancedSettings, &QAction::triggered, this, &MainWindow::onAdvancedSettingsButtonClicked);
|
||||
connect(ui->actionSavePlaylist, &QAction::triggered, this, &MainWindow::onActionSavePlaylist);
|
||||
connect(ui->actionLoadPlaylist, &QAction::triggered, this, &MainWindow::onActionLoadPlaylist);
|
||||
connect(ui->actionAppendPlaylist, &QAction::triggered, this, &MainWindow::onActionAppendPlaylist);
|
||||
connect(ui->actionSaveSong, &QAction::triggered, this, &MainWindow::onActionSaveSong);
|
||||
connect(ui->positionSlider, &QSlider::sliderMoved, this, &MainWindow::onPositionSliderSliderMoved);
|
||||
connect(ui->actionQuit, &QAction::triggered, this, [this]()
|
||||
{
|
||||
close();
|
||||
|
|
@ -59,16 +75,16 @@ MainWindow::MainWindow(QWidget *parent)
|
|||
connect(audioPlayer, &AudioPlayer::playbackStarted, this, &MainWindow::playbackStarted);
|
||||
connect(audioPlayer, &AudioPlayer::positionChanged, this, &MainWindow::updatePosition);
|
||||
connect(audioPlayer, &AudioPlayer::durationChanged, this, &MainWindow::updateDuration);
|
||||
connect(aceStep, &AceStep::songGenerated, this, &MainWindow::songGenerated);
|
||||
connect(aceStep, &AceStep::generationCancled, this, &MainWindow::generationCanceld);
|
||||
connect(aceStep, &AceStep::generationError, this, &MainWindow::generationError);
|
||||
connect(aceStep, &AceStep::progressUpdate, ui->progressBar, &QProgressBar::setValue);
|
||||
connect(aceStep, &AceStepWorker::songGenerated, this, &MainWindow::songGenerated);
|
||||
connect(aceStep, &AceStepWorker::generationCanceled, this, &MainWindow::generationCanceld);
|
||||
connect(aceStep, &AceStepWorker::generationError, this, &MainWindow::generationError);
|
||||
connect(aceStep, &AceStepWorker::progressUpdate, ui->progressBar, &QProgressBar::setValue);
|
||||
|
||||
// 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::onSongListViewDoubleClicked);
|
||||
|
||||
// Connect audio player error signal
|
||||
connect(audioPlayer, &AudioPlayer::playbackError, [this](const QString &error)
|
||||
connect(audioPlayer, &AudioPlayer::playbackError, this, [this](const QString &error)
|
||||
{
|
||||
QMessageBox::warning(this, "Playback Error", "Failed to play audio: " + error);
|
||||
});
|
||||
|
|
@ -95,11 +111,20 @@ MainWindow::MainWindow(QWidget *parent)
|
|||
ui->nowPlayingLabel->setText("Now Playing:");
|
||||
|
||||
currentSong = songModel->getSong(0);
|
||||
|
||||
// Set default volume (50% from UI)
|
||||
audioPlayer->setVolume(ui->volumeSlider->value());
|
||||
|
||||
// Start the worker thread and enter its event loop
|
||||
QObject::connect(&aceThread, &QThread::started, [this]() {qDebug() << "Worker thread started";});
|
||||
aceThread.start();
|
||||
}
|
||||
|
||||
MainWindow::~MainWindow()
|
||||
{
|
||||
aceStep->cancleGenerateion();
|
||||
aceStep->cancelGeneration();
|
||||
aceThread.quit();
|
||||
aceThread.wait();
|
||||
|
||||
autoSavePlaylist();
|
||||
saveSettings();
|
||||
|
|
@ -140,15 +165,26 @@ void MainWindow::loadSettings()
|
|||
shuffleMode = settings.value("shuffleMode", false).toBool();
|
||||
ui->shuffleButton->setChecked(shuffleMode);
|
||||
|
||||
// Load volume setting
|
||||
int savedVolume = settings.value("volume", 50).toInt();
|
||||
ui->volumeSlider->setValue(savedVolume);
|
||||
|
||||
// Load path settings with defaults based on application directory
|
||||
QString appDir = QCoreApplication::applicationDirPath();
|
||||
aceStepPath = settings.value("aceStepPath", appDir + "/acestep.cpp").toString();
|
||||
qwen3ModelPath = settings.value("qwen3ModelPath",
|
||||
appDir + "/acestep.cpp/models/acestep-5Hz-lm-4B-Q8_0.gguf").toString();
|
||||
textEncoderModelPath = settings.value("textEncoderModelPath",
|
||||
appDir + "/acestep.cpp/models/Qwen3-Embedding-0.6B-BF16.gguf").toString();
|
||||
ditModelPath = settings.value("ditModelPath", appDir + "/acestep.cpp/models/acestep-v15-turbo-Q8_0.gguf").toString();
|
||||
vaeModelPath = settings.value("vaeModelPath", appDir + "/acestep.cpp/models/vae-BF16.gguf").toString();
|
||||
|
||||
// Load low VRAM mode
|
||||
bool lowVram = settings.value("lowVramMode", false).toBool();
|
||||
aceStep->setLowVramMode(lowVram);
|
||||
|
||||
// Load flash attention setting
|
||||
bool flashAttention = settings.value("flashAttention", false).toBool();
|
||||
aceStep->setFlashAttention(flashAttention);
|
||||
}
|
||||
|
||||
void MainWindow::saveSettings()
|
||||
|
|
@ -161,13 +197,21 @@ void MainWindow::saveSettings()
|
|||
// Save shuffle mode
|
||||
settings.setValue("shuffleMode", shuffleMode);
|
||||
|
||||
// Save volume setting
|
||||
settings.setValue("volume", ui->volumeSlider->value());
|
||||
|
||||
// Save path settings
|
||||
settings.setValue("aceStepPath", aceStepPath);
|
||||
settings.setValue("qwen3ModelPath", qwen3ModelPath);
|
||||
settings.setValue("textEncoderModelPath", textEncoderModelPath);
|
||||
settings.setValue("ditModelPath", ditModelPath);
|
||||
settings.setValue("vaeModelPath", vaeModelPath);
|
||||
|
||||
// Save low VRAM mode
|
||||
settings.setValue("lowVramMode", aceStep->isLowVramMode());
|
||||
|
||||
// Save flash attention setting
|
||||
settings.setValue("flashAttention", aceStep->isFlashAttention());
|
||||
|
||||
settings.setValue("firstRun", false);
|
||||
}
|
||||
|
||||
|
|
@ -216,7 +260,7 @@ void MainWindow::updateControls()
|
|||
ui->removeSongButton->setEnabled(hasSongs && ui->songListView->currentIndex().isValid());
|
||||
}
|
||||
|
||||
void MainWindow::on_playButton_clicked()
|
||||
void MainWindow::onPlayButtonClicked()
|
||||
{
|
||||
if (isPaused)
|
||||
{
|
||||
|
|
@ -237,9 +281,9 @@ void MainWindow::on_playButton_clicked()
|
|||
updateControls();
|
||||
}
|
||||
|
||||
void MainWindow::on_pauseButton_clicked()
|
||||
void MainWindow::onPauseButtonClicked()
|
||||
{
|
||||
if (isPlaying && !isPaused)
|
||||
if (isPlaying && !isPaused && audioPlayer->isPlaying())
|
||||
{
|
||||
// Pause playback
|
||||
audioPlayer->pause();
|
||||
|
|
@ -248,7 +292,7 @@ void MainWindow::on_pauseButton_clicked()
|
|||
}
|
||||
}
|
||||
|
||||
void MainWindow::on_skipButton_clicked()
|
||||
void MainWindow::onSkipButtonClicked()
|
||||
{
|
||||
if (isPlaying)
|
||||
{
|
||||
|
|
@ -258,7 +302,7 @@ void MainWindow::on_skipButton_clicked()
|
|||
}
|
||||
}
|
||||
|
||||
void MainWindow::on_stopButton_clicked()
|
||||
void MainWindow::onStopButtonClicked()
|
||||
{
|
||||
if (isPlaying)
|
||||
{
|
||||
|
|
@ -272,7 +316,7 @@ void MainWindow::on_stopButton_clicked()
|
|||
}
|
||||
}
|
||||
|
||||
void MainWindow::on_shuffleButton_clicked()
|
||||
void MainWindow::onShuffleButtonClicked()
|
||||
{
|
||||
shuffleMode = ui->shuffleButton->isChecked();
|
||||
updateControls();
|
||||
|
|
@ -282,18 +326,13 @@ void MainWindow::on_shuffleButton_clicked()
|
|||
ensureSongsInQueue();
|
||||
}
|
||||
|
||||
void MainWindow::on_addSongButton_clicked()
|
||||
void MainWindow::onAddSongButtonClicked()
|
||||
{
|
||||
SongDialog dialog(this);
|
||||
|
||||
if (dialog.exec() == QDialog::Accepted)
|
||||
{
|
||||
QString caption = dialog.getCaption();
|
||||
QString lyrics = dialog.getLyrics();
|
||||
QString vocalLanguage = dialog.getVocalLanguage();
|
||||
|
||||
SongItem newSong(caption, lyrics);
|
||||
newSong.vocalLanguage = vocalLanguage;
|
||||
SongItem newSong = dialog.getSong();
|
||||
songModel->addSong(newSong);
|
||||
|
||||
// Select the new item
|
||||
|
|
@ -302,17 +341,18 @@ void MainWindow::on_addSongButton_clicked()
|
|||
}
|
||||
}
|
||||
|
||||
void MainWindow::on_songListView_doubleClicked(const QModelIndex &index)
|
||||
void MainWindow::onSongListViewDoubleClicked(const QModelIndex &index)
|
||||
{
|
||||
if (!index.isValid())
|
||||
return;
|
||||
|
||||
disconnect(ui->songListView, &QTableView::doubleClicked, this, &MainWindow::on_songListView_doubleClicked);
|
||||
disconnect(ui->songListView, &QTableView::doubleClicked, this, &MainWindow::onSongListViewDoubleClicked);
|
||||
|
||||
int row = index.row();
|
||||
|
||||
if (index.column() == 0)
|
||||
{
|
||||
isPaused = false;
|
||||
if (isPlaying)
|
||||
{
|
||||
audioPlayer->stop();
|
||||
|
|
@ -320,8 +360,8 @@ void MainWindow::on_songListView_doubleClicked(const QModelIndex &index)
|
|||
else
|
||||
{
|
||||
isPlaying = true;
|
||||
updateControls();
|
||||
}
|
||||
updateControls();
|
||||
|
||||
flushGenerationQueue();
|
||||
ui->nowPlayingLabel->setText("Now Playing: Waiting for generation...");
|
||||
|
|
@ -332,24 +372,16 @@ void MainWindow::on_songListView_doubleClicked(const QModelIndex &index)
|
|||
{
|
||||
SongItem song = songModel->getSong(row);
|
||||
|
||||
SongDialog dialog(this, song.caption, song.lyrics, song.vocalLanguage);
|
||||
SongDialog dialog(this, song);
|
||||
|
||||
if (dialog.exec() == QDialog::Accepted)
|
||||
{
|
||||
QString caption = dialog.getCaption();
|
||||
QString lyrics = dialog.getLyrics();
|
||||
QString vocalLanguage = dialog.getVocalLanguage();
|
||||
|
||||
songModel->setData(songModel->index(row, 1), caption, SongListModel::CaptionRole);
|
||||
songModel->setData(songModel->index(row, 1), lyrics, SongListModel::LyricsRole);
|
||||
songModel->setData(songModel->index(row, 1), vocalLanguage, SongListModel::VocalLanguageRole);
|
||||
}
|
||||
songModel->updateSong(songModel->index(row, 1), dialog.getSong());
|
||||
}
|
||||
|
||||
connect(ui->songListView, &QTableView::doubleClicked, this, &MainWindow::on_songListView_doubleClicked);
|
||||
connect(ui->songListView, &QTableView::doubleClicked, this, &MainWindow::onSongListViewDoubleClicked);
|
||||
}
|
||||
|
||||
void MainWindow::on_removeSongButton_clicked()
|
||||
void MainWindow::onRemoveSongButtonClicked()
|
||||
{
|
||||
QModelIndex currentIndex = ui->songListView->currentIndex();
|
||||
if (!currentIndex.isValid())
|
||||
|
|
@ -369,17 +401,18 @@ void MainWindow::on_removeSongButton_clicked()
|
|||
}
|
||||
}
|
||||
|
||||
void MainWindow::on_advancedSettingsButton_clicked()
|
||||
void MainWindow::onAdvancedSettingsButtonClicked()
|
||||
{
|
||||
AdvancedSettingsDialog dialog(this);
|
||||
|
||||
// Set current values
|
||||
dialog.setJsonTemplate(jsonTemplate);
|
||||
dialog.setAceStepPath(aceStepPath);
|
||||
dialog.setQwen3ModelPath(qwen3ModelPath);
|
||||
dialog.setTextEncoderModelPath(textEncoderModelPath);
|
||||
dialog.setDiTModelPath(ditModelPath);
|
||||
dialog.setVAEModelPath(vaeModelPath);
|
||||
dialog.setLowVramMode(aceStep->isLowVramMode());
|
||||
dialog.setFlashAttention(aceStep->isFlashAttention());
|
||||
|
||||
if (dialog.exec() == QDialog::Accepted)
|
||||
{
|
||||
|
|
@ -394,12 +427,20 @@ void MainWindow::on_advancedSettingsButton_clicked()
|
|||
|
||||
// Update settings
|
||||
jsonTemplate = dialog.getJsonTemplate();
|
||||
aceStepPath = dialog.getAceStepPath();
|
||||
qwen3ModelPath = dialog.getQwen3ModelPath();
|
||||
textEncoderModelPath = dialog.getTextEncoderModelPath();
|
||||
ditModelPath = dialog.getDiTModelPath();
|
||||
vaeModelPath = dialog.getVAEModelPath();
|
||||
|
||||
// Update model paths for acestep.cpp
|
||||
aceStep->setModelPaths(qwen3ModelPath, textEncoderModelPath, ditModelPath, vaeModelPath);
|
||||
|
||||
// Update low VRAM mode
|
||||
aceStep->setLowVramMode(dialog.getLowVramMode());
|
||||
|
||||
// Update flash attention setting
|
||||
aceStep->setFlashAttention(dialog.getFlashAttention());
|
||||
|
||||
saveSettings();
|
||||
}
|
||||
}
|
||||
|
|
@ -412,11 +453,19 @@ void MainWindow::playbackStarted()
|
|||
void MainWindow::playSong(const SongItem& song)
|
||||
{
|
||||
currentSong = song;
|
||||
audioPlayer->play(song.file);
|
||||
if (song.audioData)
|
||||
{
|
||||
audioPlayer->play(song.audioData);
|
||||
}
|
||||
else if (!song.file.isEmpty())
|
||||
{
|
||||
audioPlayer->play(song.file);
|
||||
}
|
||||
songModel->setPlayingIndex(songModel->findSongIndexById(song.uniqueId));
|
||||
ui->nowPlayingLabel->setText("Now Playing: " + song.caption);
|
||||
ui->lyricsTextEdit->setPlainText(song.lyrics);
|
||||
ui->jsonTextEdit->setPlainText(song.json);
|
||||
updateControls();
|
||||
}
|
||||
|
||||
void MainWindow::songGenerated(const SongItem& song)
|
||||
|
|
@ -502,7 +551,7 @@ void MainWindow::updatePlaybackStatus(bool playing)
|
|||
updateControls();
|
||||
}
|
||||
|
||||
void MainWindow::on_positionSlider_sliderMoved(int position)
|
||||
void MainWindow::onPositionSliderSliderMoved(int position)
|
||||
{
|
||||
if (isPlaying && audioPlayer->isPlaying())
|
||||
{
|
||||
|
|
@ -510,6 +559,11 @@ void MainWindow::on_positionSlider_sliderMoved(int position)
|
|||
}
|
||||
}
|
||||
|
||||
void MainWindow::onVolumeSliderValueChanged(int value)
|
||||
{
|
||||
audioPlayer->setVolume(value);
|
||||
}
|
||||
|
||||
void MainWindow::ensureSongsInQueue(bool enqeueCurrent)
|
||||
{
|
||||
// Only generate more songs if we're playing and not already at capacity
|
||||
|
|
@ -520,7 +574,7 @@ void MainWindow::ensureSongsInQueue(bool enqeueCurrent)
|
|||
|
||||
SongItem lastSong;
|
||||
SongItem workerSong;
|
||||
if(aceStep->isGenerateing(&workerSong))
|
||||
if(aceStep->isGenerating(&workerSong))
|
||||
lastSong = workerSong;
|
||||
else if(!generatedSongQueue.empty())
|
||||
lastSong = generatedSongQueue.last();
|
||||
|
|
@ -541,21 +595,18 @@ void MainWindow::ensureSongsInQueue(bool enqeueCurrent)
|
|||
isGeneratingNext = true;
|
||||
|
||||
ui->statusbar->showMessage("Generateing: "+nextSong.caption);
|
||||
aceStep->requestGeneration(nextSong, jsonTemplate,
|
||||
aceStepPath, qwen3ModelPath,
|
||||
textEncoderModelPath, ditModelPath,
|
||||
vaeModelPath);
|
||||
aceStep->requestGeneration(nextSong, jsonTemplate);
|
||||
}
|
||||
|
||||
void MainWindow::flushGenerationQueue()
|
||||
{
|
||||
generatedSongQueue.clear();
|
||||
aceStep->cancleGenerateion();
|
||||
aceStep->cancelGeneration();
|
||||
isGeneratingNext = false;
|
||||
}
|
||||
|
||||
// Playlist save/load methods
|
||||
void MainWindow::on_actionSavePlaylist()
|
||||
void MainWindow::onActionSavePlaylist()
|
||||
{
|
||||
QString filePath = QFileDialog::getSaveFileName(this, "Save Playlist",
|
||||
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/playlist.json",
|
||||
|
|
@ -567,7 +618,7 @@ void MainWindow::on_actionSavePlaylist()
|
|||
}
|
||||
}
|
||||
|
||||
void MainWindow::on_actionLoadPlaylist()
|
||||
void MainWindow::onActionLoadPlaylist()
|
||||
{
|
||||
QString filePath = QFileDialog::getOpenFileName(this, "Load Playlist",
|
||||
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation),
|
||||
|
|
@ -580,7 +631,7 @@ void MainWindow::on_actionLoadPlaylist()
|
|||
}
|
||||
}
|
||||
|
||||
void MainWindow::on_actionAppendPlaylist()
|
||||
void MainWindow::onActionAppendPlaylist()
|
||||
{
|
||||
QString filePath = QFileDialog::getOpenFileName(this, "Load Playlist",
|
||||
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation),
|
||||
|
|
@ -591,7 +642,7 @@ void MainWindow::on_actionAppendPlaylist()
|
|||
}
|
||||
}
|
||||
|
||||
void MainWindow::on_actionSaveSong()
|
||||
void MainWindow::onActionSaveSong()
|
||||
{
|
||||
QString filePath = QFileDialog::getSaveFileName(this, "Save Playlist",
|
||||
QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/song.json",
|
||||
|
|
@ -615,7 +666,22 @@ void MainWindow::on_actionSaveSong()
|
|||
QFile file(filePath);
|
||||
if (!file.open(QIODevice::WriteOnly | QIODevice::Text))
|
||||
return;
|
||||
QFile::copy(currentSong.file, filePath + ".wav");
|
||||
|
||||
// Save audio from memory if available, otherwise fall back to file
|
||||
if (currentSong.audioData)
|
||||
{
|
||||
QFile wavFile(filePath + ".wav");
|
||||
if (wavFile.open(QIODevice::WriteOnly))
|
||||
{
|
||||
wavFile.write(*currentSong.audioData);
|
||||
wavFile.close();
|
||||
}
|
||||
}
|
||||
else if (!currentSong.file.isEmpty())
|
||||
{
|
||||
QFile::copy(currentSong.file, filePath + ".wav");
|
||||
}
|
||||
|
||||
file.write(jsonData);
|
||||
file.close();
|
||||
}
|
||||
|
|
@ -692,16 +758,13 @@ bool MainWindow::savePlaylistToJson(const QString &filePath, const QList<SongIte
|
|||
for (const SongItem &song : songs)
|
||||
{
|
||||
QJsonObject songObj;
|
||||
songObj["caption"] = song.caption;
|
||||
songObj["lyrics"] = song.lyrics;
|
||||
songObj["vocalLanguage"] = song.vocalLanguage;
|
||||
songObj["uniqueId"] = static_cast<qint64>(song.uniqueId); // Store as qint64 for JSON compatibility
|
||||
song.store(songObj);
|
||||
songsArray.append(songObj);
|
||||
}
|
||||
|
||||
QJsonObject rootObj;
|
||||
rootObj["songs"] = songsArray;
|
||||
rootObj["version"] = "1.0";
|
||||
rootObj["version"] = "1.1";
|
||||
|
||||
QJsonDocument doc(rootObj);
|
||||
QByteArray jsonData = doc.toJson();
|
||||
|
|
@ -750,8 +813,7 @@ bool MainWindow::loadPlaylistFromJson(const QString &filePath, QList<SongItem> &
|
|||
|
||||
QJsonObject rootObj = doc.object();
|
||||
|
||||
// Check for version compatibility
|
||||
if (rootObj.contains("version") && rootObj["version"].toString() != "1.0")
|
||||
if (rootObj.contains("version") && rootObj["version"].toString() != "1.0" && rootObj["version"].toString() != "1.1")
|
||||
{
|
||||
qWarning() << "Unsupported playlist version:" << rootObj["version"].toString();
|
||||
return false;
|
||||
|
|
@ -773,35 +835,7 @@ bool MainWindow::loadPlaylistFromJson(const QString &filePath, QList<SongItem> &
|
|||
continue;
|
||||
|
||||
QJsonObject songObj = value.toObject();
|
||||
SongItem song;
|
||||
|
||||
if (songObj.contains("caption"))
|
||||
{
|
||||
song.caption = songObj["caption"].toString();
|
||||
}
|
||||
|
||||
if (songObj.contains("lyrics"))
|
||||
{
|
||||
song.lyrics = songObj["lyrics"].toString();
|
||||
}
|
||||
|
||||
// Load vocalLanguage if present
|
||||
if (songObj.contains("vocalLanguage"))
|
||||
{
|
||||
song.vocalLanguage = songObj["vocalLanguage"].toString();
|
||||
}
|
||||
|
||||
// Load uniqueId if present (for backward compatibility)
|
||||
if (songObj.contains("uniqueId"))
|
||||
{
|
||||
song.uniqueId = static_cast<uint64_t>(songObj["uniqueId"].toInteger());
|
||||
}
|
||||
else
|
||||
{
|
||||
// Generate new ID for old playlists without uniqueId
|
||||
song.uniqueId = QRandomGenerator::global()->generate64();
|
||||
}
|
||||
|
||||
SongItem song(songObj);
|
||||
songs.append(song);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
/*
|
||||
* Copyright Carl Philipp Klemm 2026
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef MAINWINDOW_H
|
||||
#define MAINWINDOW_H
|
||||
|
||||
|
|
@ -31,7 +36,7 @@ class MainWindow : public QMainWindow
|
|||
SongListModel *songModel;
|
||||
AudioPlayer *audioPlayer;
|
||||
QThread aceThread;
|
||||
AceStep *aceStep;
|
||||
AceStepWorker *aceStep;
|
||||
QTimer *playbackTimer;
|
||||
|
||||
QString formatTime(int milliseconds);
|
||||
|
|
@ -45,7 +50,6 @@ class MainWindow : public QMainWindow
|
|||
QString jsonTemplate;
|
||||
|
||||
// Path settings
|
||||
QString aceStepPath;
|
||||
QString qwen3ModelPath;
|
||||
QString textEncoderModelPath;
|
||||
QString ditModelPath;
|
||||
|
|
@ -63,19 +67,20 @@ public slots:
|
|||
void show();
|
||||
|
||||
private slots:
|
||||
void on_playButton_clicked();
|
||||
void on_pauseButton_clicked();
|
||||
void on_skipButton_clicked();
|
||||
void on_stopButton_clicked();
|
||||
void on_shuffleButton_clicked();
|
||||
void on_positionSlider_sliderMoved(int position);
|
||||
void onPlayButtonClicked();
|
||||
void onPauseButtonClicked();
|
||||
void onSkipButtonClicked();
|
||||
void onStopButtonClicked();
|
||||
void onShuffleButtonClicked();
|
||||
void onPositionSliderSliderMoved(int position);
|
||||
void onVolumeSliderValueChanged(int value);
|
||||
void updatePosition(int position);
|
||||
void updateDuration(int duration);
|
||||
void on_addSongButton_clicked();
|
||||
void on_removeSongButton_clicked();
|
||||
void on_advancedSettingsButton_clicked();
|
||||
void onAddSongButtonClicked();
|
||||
void onRemoveSongButtonClicked();
|
||||
void onAdvancedSettingsButtonClicked();
|
||||
|
||||
void on_songListView_doubleClicked(const QModelIndex &index);
|
||||
void onSongListViewDoubleClicked(const QModelIndex &index);
|
||||
|
||||
void songGenerated(const SongItem& song);
|
||||
void generationCanceld(const SongItem& song);
|
||||
|
|
@ -84,10 +89,10 @@ private slots:
|
|||
void updatePlaybackStatus(bool playing);
|
||||
void generationError(const QString &error);
|
||||
|
||||
void on_actionSavePlaylist();
|
||||
void on_actionLoadPlaylist();
|
||||
void on_actionAppendPlaylist();
|
||||
void on_actionSaveSong();
|
||||
void onActionSavePlaylist();
|
||||
void onActionLoadPlaylist();
|
||||
void onActionAppendPlaylist();
|
||||
void onActionSaveSong();
|
||||
|
||||
private:
|
||||
void loadSettings();
|
||||
|
|
|
|||
|
|
@ -178,6 +178,9 @@
|
|||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="pauseButton">
|
||||
<property name="enabled">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Pause</string>
|
||||
</property>
|
||||
|
|
@ -220,13 +223,55 @@
|
|||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<spacer name="horizontalSpacer_3">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<width>15</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="volumeLabel">
|
||||
<property name="text">
|
||||
<string>Volume:</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QSlider" name="volumeSlider">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>100</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="maximum">
|
||||
<number>100</number>
|
||||
</property>
|
||||
<property name="value">
|
||||
<number>50</number>
|
||||
</property>
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<spacer name="horizontalSpacer_2">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Policy::Fixed</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>35</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
|
|
|
|||
|
|
@ -1,48 +1,142 @@
|
|||
// Copyright Carl Philipp Klemm 2026
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "SongDialog.h"
|
||||
#include "ui_SongDialog.h"
|
||||
#include <QMessageBox>
|
||||
|
||||
SongDialog::SongDialog(QWidget *parent, const QString &caption, const QString &lyrics, const QString &vocalLanguage)
|
||||
SongDialog::SongDialog(QWidget *parent, const SongItem &song)
|
||||
: QDialog(parent),
|
||||
ui(new Ui::SongDialog)
|
||||
song(song),
|
||||
ui(new Ui::SongDialog)
|
||||
{
|
||||
ui->setupUi(this);
|
||||
|
||||
// Set initial values if provided
|
||||
if (!caption.isEmpty())
|
||||
{
|
||||
ui->captionEdit->setPlainText(caption);
|
||||
}
|
||||
if (!lyrics.isEmpty())
|
||||
{
|
||||
ui->lyricsEdit->setPlainText(lyrics);
|
||||
}
|
||||
// Connect signals and slots explicitly
|
||||
connect(ui->okButton, &QPushButton::clicked, this, &SongDialog::onOkButtonClicked);
|
||||
connect(ui->cancelButton, &QPushButton::clicked, this, &SongDialog::onCancelButtonClicked);
|
||||
|
||||
ui->captionEdit->setPlainText(song.caption);
|
||||
ui->lyricsEdit->setPlainText(song.lyrics);
|
||||
ui->checkBoxEnhanceCaption->setChecked(song.cotCaption);
|
||||
|
||||
// Setup vocal language combo box
|
||||
ui->vocalLanguageCombo->addItem("--", ""); // Unset
|
||||
ui->vocalLanguageCombo->addItem("--", "");
|
||||
ui->vocalLanguageCombo->addItem("English (en)", "en");
|
||||
ui->vocalLanguageCombo->addItem("German (de)", "de");
|
||||
ui->vocalLanguageCombo->addItem("French (fr)", "fr");
|
||||
ui->vocalLanguageCombo->addItem("Spanish (es)", "es");
|
||||
ui->vocalLanguageCombo->addItem("Japanese (ja)", "ja");
|
||||
ui->vocalLanguageCombo->addItem("Chinese (zh)", "zh");
|
||||
ui->vocalLanguageCombo->addItem("Italian (it)", "it");
|
||||
ui->vocalLanguageCombo->addItem("Japanese (ja)", "ja");
|
||||
ui->vocalLanguageCombo->addItem("Korean (ko)", "ko");
|
||||
ui->vocalLanguageCombo->addItem("Spanish (es)", "es");
|
||||
ui->vocalLanguageCombo->addItem("French (fr)", "fr");
|
||||
ui->vocalLanguageCombo->addItem("German (de)", "de");
|
||||
ui->vocalLanguageCombo->addItem("Portuguese (pt)", "pt");
|
||||
ui->vocalLanguageCombo->addItem("Russian (ru)", "ru");
|
||||
ui->vocalLanguageCombo->addItem("Italian (it)", "it");
|
||||
ui->vocalLanguageCombo->addItem("Arabic (ar)", "ar");
|
||||
ui->vocalLanguageCombo->addItem("Azerbaijani (az)", "az");
|
||||
ui->vocalLanguageCombo->addItem("Bulgarian (bg)", "bg");
|
||||
ui->vocalLanguageCombo->addItem("Bengali (bn)", "bn");
|
||||
ui->vocalLanguageCombo->addItem("Catalan (ca)", "ca");
|
||||
ui->vocalLanguageCombo->addItem("Czech (cs)", "cs");
|
||||
ui->vocalLanguageCombo->addItem("Danish (da)", "da");
|
||||
ui->vocalLanguageCombo->addItem("Greek (el)", "el");
|
||||
ui->vocalLanguageCombo->addItem("Persian (fa)", "fa");
|
||||
ui->vocalLanguageCombo->addItem("Finnish (fi)", "fi");
|
||||
ui->vocalLanguageCombo->addItem("Hebrew (he)", "he");
|
||||
ui->vocalLanguageCombo->addItem("Hindi (hi)", "hi");
|
||||
ui->vocalLanguageCombo->addItem("Croatian (hr)", "hr");
|
||||
ui->vocalLanguageCombo->addItem("Haitian Creole (ht)", "ht");
|
||||
ui->vocalLanguageCombo->addItem("Hungarian (hu)", "hu");
|
||||
ui->vocalLanguageCombo->addItem("Indonesian (id)", "id");
|
||||
ui->vocalLanguageCombo->addItem("Icelandic (is)", "is");
|
||||
ui->vocalLanguageCombo->addItem("Latin (la)", "la");
|
||||
ui->vocalLanguageCombo->addItem("Lithuanian (lt)", "lt");
|
||||
ui->vocalLanguageCombo->addItem("Malay (ms)", "ms");
|
||||
ui->vocalLanguageCombo->addItem("Nepali (ne)", "ne");
|
||||
ui->vocalLanguageCombo->addItem("Dutch (nl)", "nl");
|
||||
ui->vocalLanguageCombo->addItem("Norwegian (no)", "no");
|
||||
ui->vocalLanguageCombo->addItem("Punjabi (pa)", "pa");
|
||||
ui->vocalLanguageCombo->addItem("Polish (pl)", "pl");
|
||||
ui->vocalLanguageCombo->addItem("Romanian (ro)", "ro");
|
||||
ui->vocalLanguageCombo->addItem("Sanskrit (sa)", "sa");
|
||||
ui->vocalLanguageCombo->addItem("Slovak (sk)", "sk");
|
||||
ui->vocalLanguageCombo->addItem("Serbian (sr)", "sr");
|
||||
ui->vocalLanguageCombo->addItem("Swedish (sv)", "sv");
|
||||
ui->vocalLanguageCombo->addItem("Swahili (sw)", "sw");
|
||||
ui->vocalLanguageCombo->addItem("Tamil (ta)", "ta");
|
||||
ui->vocalLanguageCombo->addItem("Telugu (te)", "te");
|
||||
ui->vocalLanguageCombo->addItem("Thai (th)", "th");
|
||||
ui->vocalLanguageCombo->addItem("Tagalog (tl)", "tl");
|
||||
ui->vocalLanguageCombo->addItem("Turkish (tr)", "tr");
|
||||
ui->vocalLanguageCombo->addItem("Ukrainian (uk)", "uk");
|
||||
ui->vocalLanguageCombo->addItem("Urdu (ur)", "ur");
|
||||
ui->vocalLanguageCombo->addItem("Vietnamese (vi)", "vi");
|
||||
ui->vocalLanguageCombo->addItem("Cantonese (yue)", "yue");
|
||||
ui->vocalLanguageCombo->addItem("Unknown", "unknown");
|
||||
|
||||
// Set current language if provided
|
||||
if (!vocalLanguage.isEmpty())
|
||||
ui->keyScaleCombo->addItem("--");
|
||||
ui->keyScaleCombo->addItem("C major");
|
||||
ui->keyScaleCombo->addItem("C# major");
|
||||
ui->keyScaleCombo->addItem("D major");
|
||||
ui->keyScaleCombo->addItem("D# major");
|
||||
ui->keyScaleCombo->addItem("E major");
|
||||
ui->keyScaleCombo->addItem("F major");
|
||||
ui->keyScaleCombo->addItem("F# major");
|
||||
ui->keyScaleCombo->addItem("G major");
|
||||
ui->keyScaleCombo->addItem("G# major");
|
||||
ui->keyScaleCombo->addItem("A major");
|
||||
ui->keyScaleCombo->addItem("A# major");
|
||||
ui->keyScaleCombo->addItem("B major");
|
||||
ui->keyScaleCombo->addItem("C minor");
|
||||
ui->keyScaleCombo->addItem("C# minor");
|
||||
ui->keyScaleCombo->addItem("D minor");
|
||||
ui->keyScaleCombo->addItem("D# minor");
|
||||
ui->keyScaleCombo->addItem("E minor");
|
||||
ui->keyScaleCombo->addItem("F minor");
|
||||
ui->keyScaleCombo->addItem("F# minor");
|
||||
ui->keyScaleCombo->addItem("G minor");
|
||||
ui->keyScaleCombo->addItem("G# minor");
|
||||
ui->keyScaleCombo->addItem("A minor");
|
||||
ui->keyScaleCombo->addItem("A# minor");
|
||||
ui->keyScaleCombo->addItem("B minor");
|
||||
|
||||
if (!song.vocalLanguage.isEmpty())
|
||||
{
|
||||
int index = ui->vocalLanguageCombo->findData(vocalLanguage);
|
||||
int index = ui->vocalLanguageCombo->findData(song.vocalLanguage);
|
||||
if (index >= 0)
|
||||
{
|
||||
ui->vocalLanguageCombo->setCurrentIndex(index);
|
||||
}
|
||||
else
|
||||
{
|
||||
ui->vocalLanguageCombo->addItem(song.vocalLanguage);
|
||||
ui->vocalLanguageCombo->setCurrentIndex(ui->vocalLanguageCombo->count()-1);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ui->vocalLanguageCombo->setCurrentIndex(0); // Default to unset
|
||||
ui->vocalLanguageCombo->setCurrentIndex(0);
|
||||
}
|
||||
|
||||
if (!song.key.isEmpty())
|
||||
{
|
||||
int index = ui->keyScaleCombo->findText(song.key);
|
||||
if (index >= 0)
|
||||
{
|
||||
ui->keyScaleCombo->setCurrentIndex(index);
|
||||
}
|
||||
else
|
||||
{
|
||||
ui->keyScaleCombo->addItem(song.key);
|
||||
ui->keyScaleCombo->setCurrentIndex(ui->keyScaleCombo->count()-1);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ui->keyScaleCombo->setCurrentIndex(0);
|
||||
}
|
||||
|
||||
ui->bpmSpinBox->setValue(song.bpm);
|
||||
}
|
||||
|
||||
SongDialog::~SongDialog()
|
||||
|
|
@ -50,25 +144,10 @@ SongDialog::~SongDialog()
|
|||
delete ui;
|
||||
}
|
||||
|
||||
QString SongDialog::getCaption() const
|
||||
{
|
||||
return ui->captionEdit->toPlainText();
|
||||
}
|
||||
|
||||
QString SongDialog::getLyrics() const
|
||||
{
|
||||
return ui->lyricsEdit->toPlainText();
|
||||
}
|
||||
|
||||
QString SongDialog::getVocalLanguage() const
|
||||
{
|
||||
return ui->vocalLanguageCombo->currentData().toString();
|
||||
}
|
||||
|
||||
void SongDialog::on_okButton_clicked()
|
||||
void SongDialog::onOkButtonClicked()
|
||||
{
|
||||
// Validate that caption is not empty
|
||||
QString caption = getCaption();
|
||||
QString caption = ui->captionEdit->toPlainText();
|
||||
if (caption.trimmed().isEmpty())
|
||||
{
|
||||
QMessageBox::warning(this, "Invalid Input", "Caption cannot be empty.");
|
||||
|
|
@ -78,7 +157,22 @@ void SongDialog::on_okButton_clicked()
|
|||
accept();
|
||||
}
|
||||
|
||||
void SongDialog::on_cancelButton_clicked()
|
||||
void SongDialog::onCancelButtonClicked()
|
||||
{
|
||||
reject();
|
||||
}
|
||||
}
|
||||
|
||||
const SongItem& SongDialog::getSong()
|
||||
{
|
||||
song.caption = ui->captionEdit->toPlainText();
|
||||
song.lyrics = ui->lyricsEdit->toPlainText();
|
||||
song.vocalLanguage = ui->vocalLanguageCombo->currentData().toString();
|
||||
song.cotCaption = ui->checkBoxEnhanceCaption->isChecked();
|
||||
if(ui->keyScaleCombo->currentIndex() > 0)
|
||||
song.key = ui->keyScaleCombo->currentText();
|
||||
else
|
||||
song.key = "";
|
||||
song.bpm = ui->bpmSpinBox->value();
|
||||
return song;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,16 @@
|
|||
/*
|
||||
* Copyright Carl Philipp Klemm 2026
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef SONGDIALOG_H
|
||||
#define SONGDIALOG_H
|
||||
|
||||
#include <QDialog>
|
||||
#include <QString>
|
||||
|
||||
#include "SongItem.h"
|
||||
|
||||
namespace Ui
|
||||
{
|
||||
class SongDialog;
|
||||
|
|
@ -12,19 +19,17 @@ class SongDialog;
|
|||
class SongDialog : public QDialog
|
||||
{
|
||||
Q_OBJECT
|
||||
SongItem song;
|
||||
|
||||
public:
|
||||
explicit SongDialog(QWidget *parent = nullptr, const QString &caption = "", const QString &lyrics = "",
|
||||
const QString &vocalLanguage = "");
|
||||
explicit SongDialog(QWidget *parent = nullptr, const SongItem& song = SongItem());
|
||||
~SongDialog();
|
||||
|
||||
QString getCaption() const;
|
||||
QString getLyrics() const;
|
||||
QString getVocalLanguage() const;
|
||||
const SongItem& getSong();
|
||||
|
||||
private slots:
|
||||
void on_okButton_clicked();
|
||||
void on_cancelButton_clicked();
|
||||
void onOkButtonClicked();
|
||||
void onCancelButtonClicked();
|
||||
|
||||
private:
|
||||
Ui::SongDialog *ui;
|
||||
|
|
|
|||
|
|
@ -1,109 +1,157 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>SongDialog</class>
|
||||
<widget class="QDialog" name="SongDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>500</width>
|
||||
<height>400</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Song Details</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QLabel" name="captionLabel">
|
||||
<property name="text">
|
||||
<string>Caption:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignTop</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextEdit" name="captionEdit">
|
||||
<property name="placeholderText">
|
||||
<string>Enter song caption (e.g., "Upbeat pop rock anthem with driving electric guitars")</string>
|
||||
</property>
|
||||
<property name="maximumHeight">
|
||||
<number>80</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="lyricsLabel">
|
||||
<property name="text">
|
||||
<string>Lyrics (optional):</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignTop</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QTextEdit" name="lyricsEdit">
|
||||
<property name="placeholderText">
|
||||
<string>Enter lyrics or leave empty for instrumental music</string>
|
||||
</property>
|
||||
<property name="minimumHeight">
|
||||
<number>150</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="vocalLanguageLabel">
|
||||
<property name="text">
|
||||
<string>Vocal Language:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignTop</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QComboBox" name="vocalLanguageCombo">
|
||||
<property name="editable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="buttonLayout">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="okButton">
|
||||
<property name="text">
|
||||
<string>OK</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="cancelButton">
|
||||
<property name="text">
|
||||
<string>Cancel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
<ui version="4.0">
|
||||
<class>SongDialog</class>
|
||||
<widget class="QDialog" name="SongDialog">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>500</width>
|
||||
<height>410</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Song Details</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout" stretch="1,0">
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="captionLabel">
|
||||
<property name="text">
|
||||
<string>Caption:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignTop</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QCheckBox" name="checkBoxEnhanceCaption">
|
||||
<property name="text">
|
||||
<string>Enhance Caption</string>
|
||||
</property>
|
||||
<property name="checked">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPlainTextEdit" name="captionEdit"/>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="lyricsLabel">
|
||||
<property name="text">
|
||||
<string>Lyrics (optional):</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignTop</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPlainTextEdit" name="lyricsEdit"/>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<property name="topMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="keyScalelabel">
|
||||
<property name="text">
|
||||
<string>Keyscale</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QComboBox" name="vocalLanguageCombo">
|
||||
<property name="editable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="vocalLanguageLabel">
|
||||
<property name="text">
|
||||
<string>Vocal Language:</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QComboBox" name="keyScaleCombo">
|
||||
<property name="editable">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string>Bpm (0 for llm chooses)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QSpinBox" name="bpmSpinBox">
|
||||
<property name="maximum">
|
||||
<number>9999</number>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="">
|
||||
<property name="spacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>0</number>
|
||||
</property>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="buttonLayout">
|
||||
<item>
|
||||
<spacer name="horizontalSpacer">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Orientation::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>40</width>
|
||||
<height>20</height>
|
||||
</size>
|
||||
</property>
|
||||
</spacer>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="okButton">
|
||||
<property name="text">
|
||||
<string>OK</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="cancelButton">
|
||||
<property name="text">
|
||||
<string>Cancel</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
||||
|
|
|
|||
43
src/SongItem.cpp
Normal file
43
src/SongItem.cpp
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
#include "SongItem.h"
|
||||
|
||||
SongItem::SongItem(const QString &caption, const QString &lyrics)
|
||||
: caption(caption), lyrics(lyrics), cotCaption(true), bpm(0)
|
||||
{
|
||||
uniqueId = QRandomGenerator::global()->generate64();
|
||||
}
|
||||
|
||||
SongItem::SongItem(const QJsonObject& json)
|
||||
{
|
||||
load(json);
|
||||
}
|
||||
|
||||
void SongItem::store(QJsonObject &json) const
|
||||
{
|
||||
if(!caption.isEmpty())
|
||||
json["caption"] = caption;
|
||||
if(!lyrics.isEmpty())
|
||||
json["lyrics"] = lyrics;
|
||||
json["unique_id"] = QString::number(uniqueId);
|
||||
json["use_cot_caption"] = cotCaption;
|
||||
if(!vocalLanguage.isEmpty())
|
||||
json["vocal_language"] = vocalLanguage;
|
||||
if(!key.isEmpty())
|
||||
json["keyscale"] = key;
|
||||
if(bpm != 0)
|
||||
json["bpm"] = static_cast<qlonglong>(bpm);
|
||||
}
|
||||
|
||||
void SongItem::load(const QJsonObject &json)
|
||||
{
|
||||
caption = json["caption"].toString();
|
||||
lyrics = json["lyrics"].toString();
|
||||
if(json.contains("unique_id"))
|
||||
uniqueId = json["unique_id"].toString().toULongLong();
|
||||
else
|
||||
uniqueId = QRandomGenerator::global()->generate64();
|
||||
cotCaption = json["use_cot_caption"].toBool(true);
|
||||
vocalLanguage = json["vocal_language"].toString();
|
||||
key = json["keyscale"].toString();
|
||||
bpm = json["bpm"].toInt(0);
|
||||
}
|
||||
|
||||
|
|
@ -1,22 +1,33 @@
|
|||
/*
|
||||
* Copyright Carl Philipp Klemm 2026
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
#include <QString>
|
||||
#include <QRandomGenerator>
|
||||
#include <cstdint>
|
||||
#include <QJsonObject>
|
||||
#include <memory>
|
||||
|
||||
class SongItem
|
||||
{
|
||||
public:
|
||||
QString caption;
|
||||
QString lyrics;
|
||||
unsigned int bpm;
|
||||
QString key;
|
||||
QString vocalLanguage;
|
||||
bool cotCaption;
|
||||
|
||||
uint64_t uniqueId;
|
||||
QString file;
|
||||
QString vocalLanguage;
|
||||
QString json;
|
||||
std::shared_ptr<QByteArray> audioData;
|
||||
|
||||
inline SongItem(const QString &caption = "", const QString &lyrics = "")
|
||||
: caption(caption), lyrics(lyrics)
|
||||
{
|
||||
// Generate a unique ID using cryptographically secure random number
|
||||
uniqueId = QRandomGenerator::global()->generate64();
|
||||
}
|
||||
SongItem(const QString &caption = "", const QString &lyrics = "");
|
||||
SongItem(const QJsonObject& json);
|
||||
|
||||
void store(QJsonObject& json) const;
|
||||
void load(const QJsonObject& json);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
// Copyright Carl Philipp Klemm 2026
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "SongListModel.h"
|
||||
#include <QApplication>
|
||||
#include <QTime>
|
||||
|
|
@ -108,6 +111,26 @@ bool SongListModel::setData(const QModelIndex &index, const QVariant &value, int
|
|||
return true;
|
||||
}
|
||||
|
||||
void SongListModel::updateSong(const QModelIndex &index, const SongItem& song)
|
||||
{
|
||||
const SongItem &oldSong = songList[index.row()];
|
||||
|
||||
if(song.caption != oldSong.caption)
|
||||
{
|
||||
emit dataChanged(index, index, {CaptionRole});
|
||||
}
|
||||
if(song.lyrics != oldSong.lyrics)
|
||||
{
|
||||
emit dataChanged(index, index, {LyricsRole});
|
||||
}
|
||||
if(song.vocalLanguage != oldSong.vocalLanguage)
|
||||
{
|
||||
emit dataChanged(index, index, {VocalLanguageRole});
|
||||
}
|
||||
|
||||
songList[index.row()] = song;
|
||||
}
|
||||
|
||||
Qt::ItemFlags SongListModel::flags(const QModelIndex &index) const
|
||||
{
|
||||
if (!index.isValid())
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
/*
|
||||
* Copyright Carl Philipp Klemm 2026
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef SONGLISTMODEL_H
|
||||
#define SONGLISTMODEL_H
|
||||
|
||||
|
|
@ -32,6 +37,7 @@ public:
|
|||
|
||||
// Editable:
|
||||
bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole) override;
|
||||
void updateSong(const QModelIndex &index, const SongItem& song);
|
||||
Qt::ItemFlags flags(const QModelIndex& index) const override;
|
||||
|
||||
// Add/remove songs
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
// Copyright Carl Philipp Klemm 2026
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "clickableslider.h"
|
||||
#include <QStyle>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
/*
|
||||
* Copyright Carl Philipp Klemm 2026
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef CLICKABLESLIDER_H
|
||||
#define CLICKABLESLIDER_H
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
// Copyright Carl Philipp Klemm 2026
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "elidedlabel.h"
|
||||
|
||||
#include <QTextLayout>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,8 @@
|
|||
/*
|
||||
* Copyright Carl Philipp Klemm 2026
|
||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef ELIDEDLABEL_H
|
||||
#define ELIDEDLABEL_H
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
// Copyright Carl Philipp Klemm 2026
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include <QApplication>
|
||||
#include <QThread>
|
||||
|
||||
|
|
|
|||
525
tests/test_acestep_worker.cpp
Normal file
525
tests/test_acestep_worker.cpp
Normal file
|
|
@ -0,0 +1,525 @@
|
|||
// Test for AceStepWorker
|
||||
// Compile with: cmake .. && make test_acestep_worker && ./test_acestep_worker
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QTimer>
|
||||
#include <QEventLoop>
|
||||
#include <QDebug>
|
||||
#include <QThread>
|
||||
#include <QSettings>
|
||||
#include <QFile>
|
||||
#include <QFileInfo>
|
||||
#include <iostream>
|
||||
#include <cassert>
|
||||
|
||||
#include "../src/AceStepWorker.h"
|
||||
|
||||
// Test result tracking
|
||||
static int testsPassed = 0;
|
||||
static int testsFailed = 0;
|
||||
static int testsSkipped = 0;
|
||||
|
||||
#define TEST(name) void test_##name()
|
||||
#define RUN_TEST(name) do { \
|
||||
std::cout << "Running " << #name << "... "; \
|
||||
test_##name(); \
|
||||
if (test_skipped) { \
|
||||
std::cout << "SKIPPED" << std::endl; \
|
||||
testsSkipped++; \
|
||||
test_skipped = false; \
|
||||
} else if (test_failed) { \
|
||||
std::cout << "FAILED" << std::endl; \
|
||||
testsFailed++; \
|
||||
test_failed = false; \
|
||||
} else { \
|
||||
std::cout << "PASSED" << std::endl; \
|
||||
testsPassed++; \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
static bool test_failed = false;
|
||||
static bool test_skipped = false;
|
||||
|
||||
#define ASSERT_TRUE(cond) do { \
|
||||
if (!(cond)) { \
|
||||
std::cout << "FAILED: " << #cond << " at line " << __LINE__ << std::endl; \
|
||||
test_failed = true; \
|
||||
return; \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
#define ASSERT_FALSE(cond) ASSERT_TRUE(!(cond))
|
||||
|
||||
#define SKIP_IF(cond) do { \
|
||||
if (cond) { \
|
||||
std::cout << "(skipping: " << #cond << ") "; \
|
||||
test_skipped = true; \
|
||||
return; \
|
||||
} \
|
||||
} while(0)
|
||||
|
||||
// Helper to get model paths from settings like main app
|
||||
struct ModelPaths {
|
||||
QString lmPath;
|
||||
QString textEncoderPath;
|
||||
QString ditPath;
|
||||
QString vaePath;
|
||||
};
|
||||
|
||||
static ModelPaths getModelPathsFromSettings()
|
||||
{
|
||||
ModelPaths paths;
|
||||
QSettings settings("MusicGenerator", "AceStepGUI");
|
||||
|
||||
QString appDir = QCoreApplication::applicationDirPath();
|
||||
paths.lmPath = settings.value("qwen3ModelPath",
|
||||
appDir + "/acestep.cpp/models/acestep-5Hz-lm-4B-Q8_0.gguf").toString();
|
||||
paths.textEncoderPath = settings.value("textEncoderModelPath",
|
||||
appDir + "/acestep.cpp/models/Qwen3-Embedding-0.6B-Q8_0.gguf").toString();
|
||||
paths.ditPath = settings.value("ditModelPath",
|
||||
appDir + "/acestep.cpp/models/acestep-v15-turbo-Q8_0.gguf").toString();
|
||||
paths.vaePath = settings.value("vaeModelPath",
|
||||
appDir + "/acestep.cpp/models/vae-BF16.gguf").toString();
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
static bool modelsExist(const ModelPaths& paths)
|
||||
{
|
||||
return QFileInfo::exists(paths.lmPath) &&
|
||||
QFileInfo::exists(paths.textEncoderPath) &&
|
||||
QFileInfo::exists(paths.ditPath) &&
|
||||
QFileInfo::exists(paths.vaePath);
|
||||
}
|
||||
|
||||
// Test 1: Check that isGenerating returns false initially
|
||||
TEST(initialState)
|
||||
{
|
||||
AceStepWorker worker;
|
||||
ASSERT_TRUE(!worker.isGenerating());
|
||||
}
|
||||
|
||||
// Test 2: Check that requestGeneration returns false when no model paths set
|
||||
TEST(noModelPaths)
|
||||
{
|
||||
AceStepWorker worker;
|
||||
SongItem song("test caption", "");
|
||||
|
||||
bool result = worker.requestGeneration(song, "{}");
|
||||
ASSERT_FALSE(result);
|
||||
ASSERT_TRUE(!worker.isGenerating());
|
||||
}
|
||||
|
||||
// Test 3: Check that setModelPaths stores paths correctly
|
||||
TEST(setModelPaths)
|
||||
{
|
||||
AceStepWorker worker;
|
||||
worker.setModelPaths("/path/lm.gguf", "/path/encoder.gguf", "/path/dit.gguf", "/path/vae.gguf");
|
||||
ASSERT_TRUE(true);
|
||||
}
|
||||
|
||||
// Test 4: Check async behavior - requestGeneration returns immediately
|
||||
TEST(asyncReturnsImmediately)
|
||||
{
|
||||
AceStepWorker worker;
|
||||
worker.setModelPaths("/path/lm.gguf", "/path/encoder.gguf", "/path/dit.gguf", "/path/vae.gguf");
|
||||
|
||||
SongItem song("test caption", "");
|
||||
|
||||
// If this blocks, the test will hang
|
||||
bool result = worker.requestGeneration(song, "{}");
|
||||
|
||||
// Should return false due to invalid paths, but immediately
|
||||
ASSERT_FALSE(result);
|
||||
}
|
||||
|
||||
// Test 5: Check that cancelGeneration sets the cancel flag
|
||||
TEST(cancellationFlag)
|
||||
{
|
||||
AceStepWorker worker;
|
||||
worker.setModelPaths("/path/lm.gguf", "/path/encoder.gguf", "/path/dit.gguf", "/path/vae.gguf");
|
||||
worker.cancelGeneration();
|
||||
ASSERT_TRUE(true);
|
||||
}
|
||||
|
||||
// Test 6: Check that signals are defined correctly
|
||||
TEST(signalsExist)
|
||||
{
|
||||
AceStepWorker worker;
|
||||
|
||||
// Verify signals exist by connecting to them (compile-time check)
|
||||
QObject::connect(&worker, &AceStepWorker::songGenerated, [](const SongItem&) {});
|
||||
QObject::connect(&worker, &AceStepWorker::generationCanceled, [](const SongItem&) {});
|
||||
QObject::connect(&worker, &AceStepWorker::generationError, [](const QString&) {});
|
||||
QObject::connect(&worker, &AceStepWorker::progressUpdate, [](int) {});
|
||||
|
||||
ASSERT_TRUE(true);
|
||||
}
|
||||
|
||||
// Test 7: Check SongItem to AceRequest conversion (internal)
|
||||
TEST(requestConversion)
|
||||
{
|
||||
AceStepWorker worker;
|
||||
|
||||
SongItem song("Upbeat pop rock", "[Verse 1]");
|
||||
song.cotCaption = true;
|
||||
|
||||
QString templateJson = R"({"inference_steps": 8, "shift": 3.0, "vocal_language": "en"})";
|
||||
|
||||
worker.setModelPaths("/path/lm.gguf", "/path/encoder.gguf", "/path/dit.gguf", "/path/vae.gguf");
|
||||
bool result = worker.requestGeneration(song, templateJson);
|
||||
|
||||
// Should fail due to invalid paths, but shouldn't crash
|
||||
ASSERT_FALSE(result);
|
||||
}
|
||||
|
||||
// Test 8: Read model paths from settings
|
||||
TEST(readSettings)
|
||||
{
|
||||
ModelPaths paths = getModelPathsFromSettings();
|
||||
|
||||
std::cout << "\n Model paths from settings:" << std::endl;
|
||||
std::cout << " LM: " << paths.lmPath.toStdString() << std::endl;
|
||||
std::cout << " Text Encoder: " << paths.textEncoderPath.toStdString() << std::endl;
|
||||
std::cout << " DiT: " << paths.ditPath.toStdString() << std::endl;
|
||||
std::cout << " VAE: " << paths.vaePath.toStdString() << std::endl;
|
||||
|
||||
ASSERT_TRUE(!paths.lmPath.isEmpty());
|
||||
ASSERT_TRUE(!paths.textEncoderPath.isEmpty());
|
||||
ASSERT_TRUE(!paths.ditPath.isEmpty());
|
||||
ASSERT_TRUE(!paths.vaePath.isEmpty());
|
||||
}
|
||||
|
||||
// Test 9: Check if model files exist
|
||||
TEST(checkModelFiles)
|
||||
{
|
||||
ModelPaths paths = getModelPathsFromSettings();
|
||||
|
||||
bool lmExists = QFileInfo::exists(paths.lmPath);
|
||||
bool encoderExists = QFileInfo::exists(paths.textEncoderPath);
|
||||
bool ditExists = QFileInfo::exists(paths.ditPath);
|
||||
bool vaeExists = QFileInfo::exists(paths.vaePath);
|
||||
|
||||
std::cout << "\n Model file status:" << std::endl;
|
||||
std::cout << " LM: " << (lmExists ? "EXISTS" : "MISSING") << std::endl;
|
||||
std::cout << " Text Encoder: " << (encoderExists ? "EXISTS" : "MISSING") << std::endl;
|
||||
std::cout << " DiT: " << (ditExists ? "EXISTS" : "MISSING") << std::endl;
|
||||
std::cout << " VAE: " << (vaeExists ? "EXISTS" : "MISSING") << std::endl;
|
||||
|
||||
ASSERT_TRUE(lmExists);
|
||||
ASSERT_TRUE(encoderExists);
|
||||
ASSERT_TRUE(ditExists);
|
||||
ASSERT_TRUE(vaeExists);
|
||||
}
|
||||
|
||||
// Test 10: Actually generate a song (requires valid model paths)
|
||||
TEST(generateSong)
|
||||
{
|
||||
ModelPaths paths = getModelPathsFromSettings();
|
||||
|
||||
// Skip if models don't exist
|
||||
SKIP_IF(!modelsExist(paths));
|
||||
|
||||
AceStepWorker worker;
|
||||
worker.setModelPaths(paths.lmPath, paths.textEncoderPath, paths.ditPath, paths.vaePath);
|
||||
|
||||
SongItem song("Upbeat pop rock with driving guitars", "");
|
||||
|
||||
QString templateJson = R"({"inference_steps": 8, "shift": 3.0, "vocal_language": "en"})";
|
||||
|
||||
// Track if we get progress updates
|
||||
bool gotProgress = false;
|
||||
QObject::connect(&worker, &AceStepWorker::progressUpdate, [&gotProgress](int p) {
|
||||
std::cout << "\n Progress: " << p << "%" << std::endl;
|
||||
gotProgress = true;
|
||||
});
|
||||
|
||||
// Track generation result
|
||||
bool generationCompleted = false;
|
||||
SongItem resultSong;
|
||||
QObject::connect(&worker, &AceStepWorker::songGenerated,
|
||||
[&generationCompleted, &resultSong](const SongItem& song) {
|
||||
std::cout << "\n Song generated successfully!" << std::endl;
|
||||
std::cout << " Caption: " << song.caption.toStdString() << std::endl;
|
||||
std::cout << " Lyrics: " << song.lyrics.left(100).toStdString() << "..." << std::endl;
|
||||
std::cout << " File: " << song.file.toStdString() << std::endl;
|
||||
resultSong = song;
|
||||
generationCompleted = true;
|
||||
});
|
||||
|
||||
QString errorMsg;
|
||||
QObject::connect(&worker, &AceStepWorker::generationError,
|
||||
[&errorMsg](const QString& err) {
|
||||
std::cout << "\n Error: " << err.toStdString() << std::endl;
|
||||
errorMsg = err;
|
||||
});
|
||||
|
||||
std::cout << "\n Starting generation..." << std::endl;
|
||||
|
||||
// Request generation
|
||||
bool result = worker.requestGeneration(song, templateJson);
|
||||
ASSERT_TRUE(result);
|
||||
|
||||
// Use QEventLoop with timer for proper event processing
|
||||
QEventLoop loop;
|
||||
QTimer timeoutTimer;
|
||||
|
||||
timeoutTimer.setSingleShot(true);
|
||||
timeoutTimer.start(300000); // 5 minute timeout
|
||||
|
||||
QObject::connect(&worker, &AceStepWorker::songGenerated, &loop, &QEventLoop::quit);
|
||||
QObject::connect(&worker, &AceStepWorker::generationError, &loop, &QEventLoop::quit);
|
||||
QObject::connect(&timeoutTimer, &QTimer::timeout, &loop, &QEventLoop::quit);
|
||||
|
||||
loop.exec();
|
||||
|
||||
ASSERT_TRUE(generationCompleted);
|
||||
ASSERT_TRUE(resultSong.audioData != nullptr);
|
||||
ASSERT_TRUE(!resultSong.audioData->isEmpty());
|
||||
|
||||
// Check audio data is not empty
|
||||
std::cout << " Audio data size: " << resultSong.audioData->size() << " bytes" << std::endl;
|
||||
ASSERT_TRUE(resultSong.audioData->size() > 1000); // Should be at least 1KB for valid audio
|
||||
}
|
||||
|
||||
// Test 11: Test cancellation
|
||||
TEST(cancellation)
|
||||
{
|
||||
ModelPaths paths = getModelPathsFromSettings();
|
||||
|
||||
// Skip if models don't exist
|
||||
SKIP_IF(!modelsExist(paths));
|
||||
|
||||
AceStepWorker worker;
|
||||
worker.setModelPaths(paths.lmPath, paths.textEncoderPath, paths.ditPath, paths.vaePath);
|
||||
|
||||
SongItem song("A very long ambient piece", "");
|
||||
|
||||
QString templateJson = R"({"inference_steps": 50, "shift": 3.0, "vocal_language": "en"})";
|
||||
|
||||
bool cancelReceived = false;
|
||||
QObject::connect(&worker, &AceStepWorker::generationCanceled,
|
||||
[&cancelReceived](const SongItem&) {
|
||||
std::cout << "\n Generation was canceled!" << std::endl;
|
||||
cancelReceived = true;
|
||||
});
|
||||
|
||||
std::cout << "\n Starting generation and will cancel after 2 seconds..." << std::endl;
|
||||
|
||||
// Start generation
|
||||
bool result = worker.requestGeneration(song, templateJson);
|
||||
ASSERT_TRUE(result);
|
||||
|
||||
// Wait 2 seconds then cancel
|
||||
QThread::sleep(2);
|
||||
worker.cancelGeneration();
|
||||
|
||||
// Wait a bit for cancellation to be processed
|
||||
QThread::sleep(1);
|
||||
QCoreApplication::processEvents();
|
||||
|
||||
// Note: cancellation may or may not complete depending on where in the process
|
||||
// the cancel was requested. The important thing is it doesn't crash.
|
||||
std::cout << " Cancel requested, no crash detected" << std::endl;
|
||||
ASSERT_TRUE(true);
|
||||
}
|
||||
|
||||
// Test 12: Test low VRAM mode generation
|
||||
TEST(generateSongLowVram)
|
||||
{
|
||||
ModelPaths paths = getModelPathsFromSettings();
|
||||
|
||||
// Skip if models don't exist
|
||||
SKIP_IF(!modelsExist(paths));
|
||||
|
||||
AceStepWorker worker;
|
||||
worker.setModelPaths(paths.lmPath, paths.textEncoderPath, paths.ditPath, paths.vaePath);
|
||||
worker.setLowVramMode(true);
|
||||
|
||||
ASSERT_TRUE(worker.isLowVramMode());
|
||||
|
||||
SongItem song("Chill electronic music", "");
|
||||
|
||||
QString templateJson = R"({"inference_steps": 8, "shift": 3.0, "vocal_language": "en"})";
|
||||
|
||||
// Track generation result
|
||||
bool generationCompleted = false;
|
||||
SongItem resultSong;
|
||||
QObject::connect(&worker, &AceStepWorker::songGenerated,
|
||||
[&generationCompleted, &resultSong](const SongItem& song) {
|
||||
std::cout << "\n Low VRAM mode: Song generated successfully!" << std::endl;
|
||||
std::cout << " Caption: " << song.caption.toStdString() << std::endl;
|
||||
if (song.audioData) {
|
||||
std::cout << " Audio data size: " << song.audioData->size() << " bytes" << std::endl;
|
||||
} else {
|
||||
std::cout << " Audio data size: null" << std::endl;
|
||||
}
|
||||
resultSong = song;
|
||||
generationCompleted = true;
|
||||
});
|
||||
|
||||
QString errorMsg;
|
||||
QObject::connect(&worker, &AceStepWorker::generationError,
|
||||
[&errorMsg](const QString& err) {
|
||||
std::cout << "\n Error: " << err.toStdString() << std::endl;
|
||||
errorMsg = err;
|
||||
});
|
||||
|
||||
std::cout << "\n Starting low VRAM mode generation..." << std::endl;
|
||||
|
||||
// Request generation
|
||||
bool result = worker.requestGeneration(song, templateJson);
|
||||
ASSERT_TRUE(result);
|
||||
|
||||
// Use QEventLoop with timer for proper event processing
|
||||
QEventLoop loop;
|
||||
QTimer timeoutTimer;
|
||||
|
||||
timeoutTimer.setSingleShot(true);
|
||||
timeoutTimer.start(300000); // 5 minute timeout
|
||||
|
||||
QObject::connect(&worker, &AceStepWorker::songGenerated, &loop, &QEventLoop::quit);
|
||||
QObject::connect(&worker, &AceStepWorker::generationError, &loop, &QEventLoop::quit);
|
||||
QObject::connect(&timeoutTimer, &QTimer::timeout, &loop, &QEventLoop::quit);
|
||||
|
||||
loop.exec();
|
||||
|
||||
ASSERT_TRUE(generationCompleted);
|
||||
ASSERT_TRUE(resultSong.audioData != nullptr);
|
||||
ASSERT_TRUE(!resultSong.audioData->isEmpty());
|
||||
|
||||
std::cout << " Audio data size: " << resultSong.audioData->size() << " bytes" << std::endl;
|
||||
ASSERT_TRUE(resultSong.audioData->size() > 1000);
|
||||
}
|
||||
|
||||
// Test 13: Test normal mode keeps models loaded between generations
|
||||
TEST(normalModeKeepsModelsLoaded)
|
||||
{
|
||||
ModelPaths paths = getModelPathsFromSettings();
|
||||
|
||||
// Skip if models don't exist
|
||||
SKIP_IF(!modelsExist(paths));
|
||||
|
||||
AceStepWorker worker;
|
||||
worker.setModelPaths(paths.lmPath, paths.textEncoderPath, paths.ditPath, paths.vaePath);
|
||||
// Normal mode is default (lowVramMode = false)
|
||||
|
||||
ASSERT_FALSE(worker.isLowVramMode());
|
||||
|
||||
QString templateJson = R"({"inference_steps": 8, "shift": 3.0, "vocal_language": "en"})";
|
||||
|
||||
// Generate first song
|
||||
bool firstGenerationCompleted = false;
|
||||
QObject::connect(&worker, &AceStepWorker::songGenerated,
|
||||
[&firstGenerationCompleted](const SongItem&) {
|
||||
firstGenerationCompleted = true;
|
||||
});
|
||||
|
||||
QObject::connect(&worker, &AceStepWorker::generationError,
|
||||
[](const QString& err) {
|
||||
std::cout << "\n Error: " << err.toStdString() << std::endl;
|
||||
});
|
||||
|
||||
std::cout << "\n Generating first song (normal mode)..." << std::endl;
|
||||
|
||||
SongItem song1("First song", "");
|
||||
bool result = worker.requestGeneration(song1, templateJson);
|
||||
ASSERT_TRUE(result);
|
||||
|
||||
QEventLoop loop;
|
||||
QTimer timeoutTimer;
|
||||
timeoutTimer.setSingleShot(true);
|
||||
timeoutTimer.start(300000);
|
||||
|
||||
QObject::connect(&worker, &AceStepWorker::songGenerated, &loop, &QEventLoop::quit);
|
||||
QObject::connect(&worker, &AceStepWorker::generationError, &loop, &QEventLoop::quit);
|
||||
QObject::connect(&timeoutTimer, &QTimer::timeout, &loop, &QEventLoop::quit);
|
||||
|
||||
loop.exec();
|
||||
|
||||
ASSERT_TRUE(firstGenerationCompleted);
|
||||
std::cout << " First generation completed, models should still be loaded" << std::endl;
|
||||
|
||||
// Generate second song - in normal mode this should be faster since models are already loaded
|
||||
bool secondGenerationCompleted = false;
|
||||
SongItem secondResult;
|
||||
QObject::connect(&worker, &AceStepWorker::songGenerated,
|
||||
[&secondGenerationCompleted, &secondResult](const SongItem& song) {
|
||||
secondGenerationCompleted = true;
|
||||
secondResult = song;
|
||||
});
|
||||
|
||||
std::cout << " Generating second song (should use cached models)..." << std::endl;
|
||||
|
||||
SongItem song2("Second song", "");
|
||||
result = worker.requestGeneration(song2, templateJson);
|
||||
ASSERT_TRUE(result);
|
||||
|
||||
QEventLoop loop2;
|
||||
QTimer timeoutTimer2;
|
||||
timeoutTimer2.setSingleShot(true);
|
||||
timeoutTimer2.start(300000);
|
||||
|
||||
QObject::connect(&worker, &AceStepWorker::songGenerated, &loop2, &QEventLoop::quit);
|
||||
QObject::connect(&worker, &AceStepWorker::generationError, &loop2, &QEventLoop::quit);
|
||||
QObject::connect(&timeoutTimer2, &QTimer::timeout, &loop2, &QEventLoop::quit);
|
||||
|
||||
loop2.exec();
|
||||
|
||||
ASSERT_TRUE(secondGenerationCompleted);
|
||||
ASSERT_TRUE(secondResult.audioData != nullptr);
|
||||
ASSERT_TRUE(!secondResult.audioData->isEmpty());
|
||||
|
||||
std::cout << " Second generation completed successfully" << std::endl;
|
||||
std::cout << " Audio data size: " << secondResult.audioData->size() << " bytes" << std::endl;
|
||||
}
|
||||
|
||||
// Test 14: Test setLowVramMode toggle
|
||||
TEST(lowVramModeToggle)
|
||||
{
|
||||
AceStepWorker worker;
|
||||
|
||||
// Default should be false (normal mode)
|
||||
ASSERT_FALSE(worker.isLowVramMode());
|
||||
|
||||
// Enable low VRAM mode
|
||||
worker.setLowVramMode(true);
|
||||
ASSERT_TRUE(worker.isLowVramMode());
|
||||
|
||||
// Disable low VRAM mode
|
||||
worker.setLowVramMode(false);
|
||||
ASSERT_FALSE(worker.isLowVramMode());
|
||||
|
||||
// Toggle again
|
||||
worker.setLowVramMode(true);
|
||||
ASSERT_TRUE(worker.isLowVramMode());
|
||||
}
|
||||
|
||||
int main(int argc, char *argv[])
|
||||
{
|
||||
QCoreApplication app(argc, argv);
|
||||
|
||||
std::cout << "=== AceStepWorker Tests ===" << std::endl;
|
||||
|
||||
RUN_TEST(initialState);
|
||||
RUN_TEST(noModelPaths);
|
||||
RUN_TEST(setModelPaths);
|
||||
RUN_TEST(asyncReturnsImmediately);
|
||||
RUN_TEST(cancellationFlag);
|
||||
RUN_TEST(signalsExist);
|
||||
RUN_TEST(requestConversion);
|
||||
RUN_TEST(readSettings);
|
||||
RUN_TEST(checkModelFiles);
|
||||
RUN_TEST(generateSong);
|
||||
RUN_TEST(cancellation);
|
||||
RUN_TEST(generateSongLowVram);
|
||||
RUN_TEST(normalModeKeepsModelsLoaded);
|
||||
RUN_TEST(lowVramModeToggle);
|
||||
|
||||
std::cout << "\n=== Results ===" << std::endl;
|
||||
std::cout << "Passed: " << testsPassed << std::endl;
|
||||
std::cout << "Skipped: " << testsSkipped << std::endl;
|
||||
std::cout << "Failed: " << testsFailed << std::endl;
|
||||
|
||||
return testsFailed > 0 ? 1 : 0;
|
||||
}
|
||||
1
third_party/acestep.cpp
vendored
Submodule
1
third_party/acestep.cpp
vendored
Submodule
|
|
@ -0,0 +1 @@
|
|||
Subproject commit d28398db0ffdb77e8ae071ff31bde8c559e7085a
|
||||
Loading…
Add table
Add a link
Reference in a new issue