diff --git a/.copywrite.hcl b/.copywrite.hcl deleted file mode 100644 index 807c217..0000000 --- a/.copywrite.hcl +++ /dev/null @@ -1,15 +0,0 @@ -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**", - ] -} diff --git a/.github/actions/linux-setup-vulkan/action.yml b/.github/actions/linux-setup-vulkan/action.yml deleted file mode 100644 index 4d29837..0000000 --- a/.github/actions/linux-setup-vulkan/action.yml +++ /dev/null @@ -1,20 +0,0 @@ -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 diff --git a/.github/actions/unarchive-tar/action.yml b/.github/actions/unarchive-tar/action.yml deleted file mode 100644 index b97e402..0000000 --- a/.github/actions/unarchive-tar/action.yml +++ /dev/null @@ -1,27 +0,0 @@ -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 }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 66e114d..5ba0ee4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,24 +6,10 @@ 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) @@ -37,9 +23,8 @@ 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 -DGGML_VULKAN=ON -DCMAKE_INSTALL_PREFIX=./install/usr -DGIT_TAG=${{ steps.version.outputs.tag }} .. + cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_INSTALL_PREFIX=./install/usr -DGIT_TAG=${{ steps.version.outputs.tag }} .. make -j $(nproc) make install - name: Create Appimage diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index f58ab56..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "third_party/acestep.cpp"] - path = third_party/acestep.cpp - url = https://github.com/ServeurpersoCom/acestep.cpp.git diff --git a/CMakeLists.txt b/CMakeLists.txt index b1f0110..c3dcea3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,3 @@ -# Copyright Carl Philipp Klemm 2026 -# SPDX-License-Identifier: GPL-3.0-or-later - cmake_minimum_required(VERSION 3.14) project(aceradio LANGUAGES CXX) @@ -10,8 +7,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) # Find Qt packages find_package(Qt6 COMPONENTS Core Gui Widgets Multimedia REQUIRED) -# Add acestep.cpp subdirectory -add_subdirectory(third_party/acestep.cpp) +# Note: acestep.cpp binaries and models should be provided at runtime set(CMAKE_AUTOMOC ON) set(CMAKE_AUTOUIC ON) @@ -41,29 +37,26 @@ 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 (Qt + acestep.cpp) +# Link libraries (only Qt libraries - acestep.cpp is external) target_link_libraries(${PROJECT_NAME} PRIVATE Qt6::Core Qt6::Gui Qt6::Widgets Qt6::Multimedia - acestep-core - ggml ) -# Include directories +# Include directories (only our source directory - acestep.cpp is external) 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) @@ -73,24 +66,3 @@ 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 -) diff --git a/LICENSE b/LICENSE deleted file mode 100644 index f288702..0000000 --- a/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - 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. - - - Copyright (C) - - 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 . - -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: - - Copyright (C) - 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 -. - - 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 -. diff --git a/README.md b/README.md index 1b7bfe9..2333021 100644 --- a/README.md +++ b/README.md @@ -3,65 +3,36 @@ ![Screenshot](res/scrot.png) 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 -### Linux / macOS +### Build acestep.cpp first: -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)` +```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) +``` -### Windows (Qt6 + CMake) +### Build the GUI: -To build on Windows run the following commands in *Command Prompt (cmd)* or *PowerShell*: - -1. Configure and Build: -```cmd +```bash git clone https://github.com/IMbackK/aceradio.git cd aceradio -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 +mkdir build && cd build +cmake .. +make -j$(nproc) ``` ## Setup Paths: -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 +Go to settings->Ace Step->Model Paths and add the paths to the acestep.cpp binaries the models. diff --git a/res/xyz.uvos.aceradio.png b/res/xyz.uvos.aceradio.png index 032045e..603d014 100644 Binary files a/res/xyz.uvos.aceradio.png and b/res/xyz.uvos.aceradio.png differ diff --git a/res/xyz.uvos.aceradio.svg b/res/xyz.uvos.aceradio.svg index 70a6ec5..23516fd 100644 --- a/res/xyz.uvos.aceradio.svg +++ b/res/xyz.uvos.aceradio.svg @@ -7,7 +7,7 @@ width="594" height="594" viewBox="0 0 594 594" - sodipodi:docname="xyz.uvos.aceradio.svg" + sodipodi:docname="icon.svg" inkscape:export-filename="icon64.png" inkscape:export-xdpi="10.343434" inkscape:export-ydpi="10.343434" @@ -28,9 +28,9 @@ inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" showguides="true" - inkscape:zoom="0.82324218" - inkscape:cx="38.8707" - inkscape:cy="407.535" + inkscape:zoom="0.58212013" + inkscape:cx="-146.01797" + inkscape:cy="490.44859" inkscape:window-width="1920" inkscape:window-height="1054" inkscape:window-x="0" @@ -45,14 +45,6 @@ - #include #include +#include #include +#include #include +#include #include -// acestep.cpp headers -#include "pipeline-lm.h" -#include "request.h" - -AceStepWorker::AceStepWorker(QObject* parent) - : QObject(parent) +AceStep::AceStep(QObject* parent): QObject(parent) { + connect(&qwenProcess, &QProcess::finished, this, &AceStep::qwenProcFinished); + connect(&ditVaeProcess, &QProcess::finished, this, &AceStep::ditProcFinished); } -AceStepWorker::~AceStepWorker() +bool AceStep::isGenerateing(SongItem* song) { - cancelGeneration(); - unloadModels(); + if(!busy && song) + *song = this->request.song; + return busy; } -void AceStepWorker::setModelPaths(QString lmPath, QString textEncoderPath, QString ditPath, QString vaePath) +void AceStep::cancleGenerateion() { - // 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()) + 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; +} + +bool AceStep::requestGeneration(SongItem song, QString requestTemplate, QString aceStepPath, + QString qwen3ModelPath, QString textEncoderModelPath, QString ditModelPath, + QString vaeModelPath) +{ + if(busy) { - unloadModels(); + qWarning()<<"Droping song:"<generate(), aceStepPath, textEncoderModelPath, ditModelPath, vaeModelPath}; -void AceStepWorker::setFlashAttention(bool enabled) -{ - m_flashAttention = enabled; -} - -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()) + QString qwen3Binary = aceStepPath + "/ace-qwen3"; + QFileInfo qwen3Info(qwen3Binary); + if (!qwen3Info.exists() || !qwen3Info.isExecutable()) { - qWarning() << "Dropping song:" << song.caption; + 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; return false; } - m_busy.store(true); - m_cancelRequested.store(false); - m_currentSong = song; - m_requestTemplate = requestTemplate; - m_uid = QRandomGenerator::global()->generate(); + request.requestFilePath = tempDir + "/request_" + QString::number(request.uid) + ".json"; - // Validate model paths - if (m_lmModelPath.isEmpty() || m_textEncoderPath.isEmpty() || - m_ditPath.isEmpty() || m_vaePath.isEmpty()) + QJsonParseError parseError; + QJsonDocument templateDoc = QJsonDocument::fromJson(requestTemplate.toUtf8(), &parseError); + if (!templateDoc.isObject()) { - emit generationError("Model paths not set. Call setModelPaths() first."); - m_busy.store(false); + generationError("Invalid JSON template: " + QString(parseError.errorString())); + busy = false; return false; } - // Validate model files exist - if (!QFileInfo::exists(m_lmModelPath)) - { - 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; - } + QJsonObject requestObj = templateDoc.object(); + requestObj["caption"] = song.caption; - // Run generation in the worker thread - QMetaObject::invokeMethod(this, "runGeneration", Qt::QueuedConnection); + 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)) + { + emit generationError("Failed to create request file: " + requestFileHandle.errorString()); + busy = 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); return true; } -bool AceStepWorker::checkCancel(void* data) +void AceStep::qwenProcFinished(int code, QProcess::ExitStatus status) { - AceStepWorker* worker = static_cast(data); - return worker->m_cancelRequested.load(); -} - -std::shared_ptr AceStepWorker::convertToWav(const AceAudio& audio) -{ - auto audioData = std::make_shared(); - - // 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(&dataSize), 4)); - audioData->append("WAVE"); - - // fmt chunk - audioData->append("fmt "); - int fmtSize = 16; - audioData->append(QByteArray::fromRawData(reinterpret_cast(&fmtSize), 4)); - short audioFormat = 1; // PCM - audioData->append(QByteArray::fromRawData(reinterpret_cast(&audioFormat), 2)); - short numCh = numChannels; - audioData->append(QByteArray::fromRawData(reinterpret_cast(&numCh), 2)); - int sampleRate = audio.sample_rate; - audioData->append(QByteArray::fromRawData(reinterpret_cast(&sampleRate), 4)); - audioData->append(QByteArray::fromRawData(reinterpret_cast(&byteRate), 4)); - audioData->append(QByteArray::fromRawData(reinterpret_cast(&blockAlign), 2)); - audioData->append(QByteArray::fromRawData(reinterpret_cast(&bitsPerSample), 2)); - - // data chunk - audioData->append("data"); - audioData->append(QByteArray::fromRawData(reinterpret_cast(&dataSize), 4)); - - // Convert float samples to 16-bit and write - QVector interleaved(audio.n_samples * numChannels); - for (int i = 0; i < audio.n_samples; i++) + QFile::remove(request.requestFilePath); + if(code != 0) { - 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(left * 32767.0f); - interleaved[i * 2 + 1] = static_cast(right * 32767.0f); - } - audioData->append(QByteArray::fromRawData(reinterpret_cast(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); + QString errorOutput = qwenProcess.readAllStandardError(); + generationError("dit-vae exited with code " + QString::number(code) + ": " + errorOutput); + busy = false; return; } - emit progressUpdate(30); - - int lmResult = ace_lm_generate(m_lmContext, &req, 1, &lmOutput, - nullptr, nullptr, - checkCancel, this, - LM_MODE_GENERATE); - - if (m_cancelRequested.load()) + QString ditVaeBinary = request.aceStepPath + "/dit-vae"; + QFileInfo ditVaeInfo(ditVaeBinary); + if (!ditVaeInfo.exists() || !ditVaeInfo.isExecutable()) { - if(m_lowVramMode) - unloadModels(); - emit generationCanceled(m_currentSong); - m_busy.store(false); + generationError("dit-vae binary not found at: " + ditVaeBinary); + busy = false; return; } - if (lmResult != 0) + request.requestLlmFilePath = tempDir + "/request_" + QString::number(request.uid) + "0.json"; + if (!QFileInfo::exists(request.requestLlmFilePath)) { - if(m_lowVramMode) - unloadModels(); - emit generationError("LM generation failed or was canceled"); - m_busy.store(false); + generationError("ace-qwen3 failed to create enhaced request file "+request.requestLlmFilePath); + busy = false; return; } - 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 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()) + // Load lyrics from the enhanced request file + QFile lmOutputFile(request.requestLlmFilePath); + if (lmOutputFile.open(QIODevice::ReadOnly | QIODevice::Text)) { QJsonParseError parseError; - QJsonDocument templateDoc = QJsonDocument::fromJson(templateJson.toUtf8(), &parseError); - if (templateDoc.isObject()) - requestJson = templateDoc.object(); - else - qWarning() << "Failed to parse request template:" << parseError.errorString(); + 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(); + } } - 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"; + // Step 2: Run dit-vae to generate audio + QStringList ditVaeArgs; + ditVaeArgs << "--request"<(QRandomGenerator::global()->generate()); + 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); +} - return req; -} \ No newline at end of file diff --git a/src/AceStepWorker.h b/src/AceStepWorker.h index 56d95b1..41c4676 100644 --- a/src/AceStepWorker.h +++ b/src/AceStepWorker.h @@ -1,98 +1,56 @@ -/* - * Copyright Carl Philipp Klemm 2026 - * SPDX-License-Identifier: GPL-3.0-or-later - */ - #ifndef ACESTEPWORKER_H #define ACESTEPWORKER_H #include #include -#include +#include #include -#include #include "SongItem.h" -#include "pipeline-synth.h" -#include "request.h" -struct AceLm; -struct AceSynth; - -class AceStepWorker : public QObject +class AceStep : public QObject { Q_OBJECT + QProcess qwenProcess; + QProcess ditVaeProcess; -public: - explicit AceStepWorker(QObject* parent = nullptr); - ~AceStepWorker(); + bool busy = false; - bool isGenerating(SongItem* song = nullptr); - void cancelGeneration(); + struct Request + { + SongItem song; + uint64_t uid; + QString aceStepPath; + QString textEncoderModelPath; + QString ditModelPath; + QString vaeModelPath; + QString requestFilePath; + QString requestLlmFilePath; + }; - // Model paths - set these before first generation - void setModelPaths(QString lmPath, QString textEncoderPath, QString ditPath, QString vaePath); + Request request; - // 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); + const QString tempDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation); signals: void songGenerated(SongItem song); - void generationCanceled(SongItem song); + void generationCancled(SongItem song); void generationError(QString error); void progressUpdate(int progress); +public slots: + bool requestGeneration(SongItem song, QString requestTemplate, QString aceStepPath, + QString qwen3ModelPath, QString textEncoderModelPath, QString ditModelPath, + QString vaeModelPath); + +public: + AceStep(QObject* parent = nullptr); + bool isGenerateing(SongItem* song = nullptr); + void cancleGenerateion(); + private slots: - void 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 convertToWav(const AceAudio& audio); - - // Generation state - std::atomic m_busy{false}; - std::atomic m_cancelRequested{false}; - std::atomic 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; + void qwenProcFinished(int code, QProcess::ExitStatus status); + void ditProcFinished(int code, QProcess::ExitStatus status); }; -#endif // ACESTEPWORKER_H \ No newline at end of file +#endif // ACESTEPWORKER_H diff --git a/src/AdvancedSettingsDialog.cpp b/src/AdvancedSettingsDialog.cpp index 7e2e412..163ba8a 100644 --- a/src/AdvancedSettingsDialog.cpp +++ b/src/AdvancedSettingsDialog.cpp @@ -1,6 +1,3 @@ -// Copyright Carl Philipp Klemm 2026 -// SPDX-License-Identifier: GPL-3.0-or-later - #include "AdvancedSettingsDialog.h" #include "ui_AdvancedSettingsDialog.h" #include @@ -13,12 +10,6 @@ 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() @@ -31,6 +22,11 @@ QString AdvancedSettingsDialog::getJsonTemplate() const return ui->jsonTemplateEdit->toPlainText(); } +QString AdvancedSettingsDialog::getAceStepPath() const +{ + return ui->aceStepPathEdit->text(); +} + QString AdvancedSettingsDialog::getQwen3ModelPath() const { return ui->qwen3ModelEdit->text(); @@ -51,21 +47,16 @@ 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); @@ -86,17 +77,16 @@ void AdvancedSettingsDialog::setVAEModelPath(const QString &path) ui->vaeModelEdit->setText(path); } -void AdvancedSettingsDialog::setLowVramMode(bool enabled) +void AdvancedSettingsDialog::on_aceStepBrowseButton_clicked() { - ui->lowVramCheckBox->setChecked(enabled); + QString dir = QFileDialog::getExistingDirectory(this, "Select AceStep Build Directory", ui->aceStepPathEdit->text()); + if (!dir.isEmpty()) + { + ui->aceStepPathEdit->setText(dir); + } } -void AdvancedSettingsDialog::setFlashAttention(bool enabled) -{ - ui->flashAttentionCheckBox->setChecked(enabled); -} - -void AdvancedSettingsDialog::onQwen3BrowseButtonClicked() +void AdvancedSettingsDialog::on_qwen3BrowseButton_clicked() { QString file = QFileDialog::getOpenFileName(this, "Select Qwen3 Model", ui->qwen3ModelEdit->text(), "GGUF Files (*.gguf)"); @@ -106,7 +96,7 @@ void AdvancedSettingsDialog::onQwen3BrowseButtonClicked() } } -void AdvancedSettingsDialog::onTextEncoderBrowseButtonClicked() +void AdvancedSettingsDialog::on_textEncoderBrowseButton_clicked() { QString file = QFileDialog::getOpenFileName(this, "Select Text Encoder Model", ui->textEncoderEdit->text(), "GGUF Files (*.gguf)"); @@ -116,7 +106,7 @@ void AdvancedSettingsDialog::onTextEncoderBrowseButtonClicked() } } -void AdvancedSettingsDialog::onDiTBrowseButtonClicked() +void AdvancedSettingsDialog::on_ditBrowseButton_clicked() { QString file = QFileDialog::getOpenFileName(this, "Select DiT Model", ui->ditModelEdit->text(), "GGUF Files (*.gguf)"); if (!file.isEmpty()) @@ -125,7 +115,7 @@ void AdvancedSettingsDialog::onDiTBrowseButtonClicked() } } -void AdvancedSettingsDialog::onVAEBrowseButtonClicked() +void AdvancedSettingsDialog::on_vaeBrowseButton_clicked() { QString file = QFileDialog::getOpenFileName(this, "Select VAE Model", ui->vaeModelEdit->text(), "GGUF Files (*.gguf)"); if (!file.isEmpty()) diff --git a/src/AdvancedSettingsDialog.h b/src/AdvancedSettingsDialog.h index 8a3047a..fb4aa69 100644 --- a/src/AdvancedSettingsDialog.h +++ b/src/AdvancedSettingsDialog.h @@ -1,8 +1,3 @@ -/* - * Copyright Carl Philipp Klemm 2026 - * SPDX-License-Identifier: GPL-3.0-or-later - */ - #ifndef ADVANCEDSETTINGSDIALOG_H #define ADVANCEDSETTINGSDIALOG_H @@ -24,27 +19,26 @@ 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 onQwen3BrowseButtonClicked(); - void onTextEncoderBrowseButtonClicked(); - void onDiTBrowseButtonClicked(); - void onVAEBrowseButtonClicked(); + void on_aceStepBrowseButton_clicked(); + void on_qwen3BrowseButton_clicked(); + void on_textEncoderBrowseButton_clicked(); + void on_ditBrowseButton_clicked(); + void on_vaeBrowseButton_clicked(); private: Ui::AdvancedSettingsDialog *ui; diff --git a/src/AdvancedSettingsDialog.ui b/src/AdvancedSettingsDialog.ui index 6e95e90..e9fadf1 100644 --- a/src/AdvancedSettingsDialog.ui +++ b/src/AdvancedSettingsDialog.ui @@ -1,239 +1,203 @@ - - AdvancedSettingsDialog - - - - 0 - 0 - 600 - 450 - - - - Advanced Settings - - - - - - 0 - - - - Performance - - - - - - Low VRAM Mode - - - - - - - Unload models between generation phases to save VRAM. Slower but uses less memory. - - - true - - - - - - - Flash Attention - - - true - - - - - - - Use flash attention for faster generation. Disable if experiencing poor output quality on vulkan. - - - true - - - - - - - Qt::Orientation::Vertical - - - - 20 - 40 - - - - - - - - - JSON Template - - - - - - JSON Template for AceStep generation: - - - true - - - - - - - - - - - Model Paths - - - - QFormLayout::FieldGrowthPolicy::AllNonFixedFieldsGrow - - - - - Qwen3 Model: - - - - - - - - - - - - Browse... - - - - - - - - - Text Encoder Model: - - - - - - - - - - - - Browse... - - - - - - - - - DiT Model: - - - - - - - - - - - - Browse... - - - - - - - - - VAE Model: - - - - - - - - - - - - Browse... - - - - - - - - - - - - - QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Save - - - - - - - - - buttonBox - accepted() - AdvancedSettingsDialog - accept() - - - 248 - 254 - - - 157 - 254 - - - - - buttonBox - rejected() - AdvancedSettingsDialog - reject() - - - 316 - 260 - - - 286 - 260 - - - - - + + AdvancedSettingsDialog + + + + 0 + 0 + 600 + 450 + + + + Advanced Settings + + + + + + 0 + + + + JSON Template + + + + + + JSON Template for AceStep generation: + + + true + + + + + + + + + + + Model Paths + + + + QFormLayout::FieldGrowthPolicy::AllNonFixedFieldsGrow + + + + + AceStep Path: + + + + + + + + + + + + Browse... + + + + + + + + + Qwen3 Model: + + + + + + + + + + + + Browse... + + + + + + + + + Text Encoder Model: + + + + + + + + + + + + Browse... + + + + + + + + + DiT Model: + + + + + + + + + + + + Browse... + + + + + + + + + VAE Model: + + + + + + + + + + + + Browse... + + + + + + + + + + + + + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Save + + + + + + + + + buttonBox + accepted() + AdvancedSettingsDialog + accept() + + + 248 + 254 + + + 157 + 254 + + + + + buttonBox + rejected() + AdvancedSettingsDialog + reject() + + + 316 + 260 + + + 286 + 260 + + + + + diff --git a/src/AudioPlayer.cpp b/src/AudioPlayer.cpp index 8374027..76694d4 100644 --- a/src/AudioPlayer.cpp +++ b/src/AudioPlayer.cpp @@ -1,8 +1,4 @@ -// Copyright Carl Philipp Klemm 2026 -// SPDX-License-Identifier: GPL-3.0-or-later - #include "AudioPlayer.h" -#include #include AudioPlayer::AudioPlayer(QObject *parent) @@ -49,33 +45,6 @@ void AudioPlayer::play(const QString &filePath) positionTimer->start(); } -void AudioPlayer::play(std::shared_ptr 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()) @@ -120,19 +89,6 @@ 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(volume) / 100.0; - audioOutput->setVolume(volumeLevel); -} - -int AudioPlayer::getVolume() const -{ - // Convert from 0.0-1.0 range to 0-100 range - return static_cast(audioOutput->volume() * 100); -} - void AudioPlayer::handlePlaybackStateChanged(QMediaPlayer::PlaybackState state) { if (state == QMediaPlayer::PlayingState) diff --git a/src/AudioPlayer.h b/src/AudioPlayer.h index 144010f..93f14b2 100644 --- a/src/AudioPlayer.h +++ b/src/AudioPlayer.h @@ -1,8 +1,3 @@ -/* - * Copyright Carl Philipp Klemm 2026 - * SPDX-License-Identifier: GPL-3.0-or-later - */ - #ifndef AUDIOPLAYER_H #define AUDIOPLAYER_H @@ -14,7 +9,6 @@ #include #include #include -#include class AudioPlayer : public QObject { @@ -24,7 +18,6 @@ public: ~AudioPlayer(); void play(const QString &filePath); - void play(std::shared_ptr audioData); void play(); void stop(); void pause(); @@ -32,8 +25,6 @@ public: bool isPlaying() const; int duration() const; int position() const; - void setVolume(int volume); - int getVolume() const; signals: void playbackStarted(); diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index 565e8a4..62c8a63 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -1,6 +1,3 @@ -// Copyright Carl Philipp Klemm 2026 -// SPDX-License-Identifier: GPL-3.0-or-later - #include "MainWindow.h" #include "ui_MainWindow.h" #include "SongDialog.h" @@ -21,7 +18,7 @@ MainWindow::MainWindow(QWidget *parent) ui(new Ui::MainWindow), songModel(new SongListModel(this)), audioPlayer(new AudioPlayer(this)), - aceStep(new AceStepWorker), + aceStep(new AceStep(this)), playbackTimer(new QTimer(this)), isPlaying(false), isPaused(false), @@ -29,7 +26,6 @@ MainWindow::MainWindow(QWidget *parent) isGeneratingNext(false) { aceStep->moveToThread(&aceThread); - aceThread.setObjectName("AceStep Woker Thread"); ui->setupUi(this); @@ -42,27 +38,15 @@ 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->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->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->actionQuit, &QAction::triggered, this, [this]() { close(); @@ -75,16 +59,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, &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(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 double-click on song list for editing (works with QTableView too) - connect(ui->songListView, &QTableView::doubleClicked, this, &MainWindow::onSongListViewDoubleClicked); + connect(ui->songListView, &QTableView::doubleClicked, this, &MainWindow::on_songListView_doubleClicked); // Connect audio player error signal - connect(audioPlayer, &AudioPlayer::playbackError, this, [this](const QString &error) + connect(audioPlayer, &AudioPlayer::playbackError, [this](const QString &error) { QMessageBox::warning(this, "Playback Error", "Failed to play audio: " + error); }); @@ -111,20 +95,11 @@ 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->cancelGeneration(); - aceThread.quit(); - aceThread.wait(); + aceStep->cancleGenerateion(); autoSavePlaylist(); saveSettings(); @@ -165,26 +140,15 @@ 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() @@ -197,21 +161,13 @@ 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); } @@ -260,7 +216,7 @@ void MainWindow::updateControls() ui->removeSongButton->setEnabled(hasSongs && ui->songListView->currentIndex().isValid()); } -void MainWindow::onPlayButtonClicked() +void MainWindow::on_playButton_clicked() { if (isPaused) { @@ -281,9 +237,9 @@ void MainWindow::onPlayButtonClicked() updateControls(); } -void MainWindow::onPauseButtonClicked() +void MainWindow::on_pauseButton_clicked() { - if (isPlaying && !isPaused && audioPlayer->isPlaying()) + if (isPlaying && !isPaused) { // Pause playback audioPlayer->pause(); @@ -292,7 +248,7 @@ void MainWindow::onPauseButtonClicked() } } -void MainWindow::onSkipButtonClicked() +void MainWindow::on_skipButton_clicked() { if (isPlaying) { @@ -302,7 +258,7 @@ void MainWindow::onSkipButtonClicked() } } -void MainWindow::onStopButtonClicked() +void MainWindow::on_stopButton_clicked() { if (isPlaying) { @@ -316,7 +272,7 @@ void MainWindow::onStopButtonClicked() } } -void MainWindow::onShuffleButtonClicked() +void MainWindow::on_shuffleButton_clicked() { shuffleMode = ui->shuffleButton->isChecked(); updateControls(); @@ -326,13 +282,18 @@ void MainWindow::onShuffleButtonClicked() ensureSongsInQueue(); } -void MainWindow::onAddSongButtonClicked() +void MainWindow::on_addSongButton_clicked() { SongDialog dialog(this); if (dialog.exec() == QDialog::Accepted) { - SongItem newSong = dialog.getSong(); + QString caption = dialog.getCaption(); + QString lyrics = dialog.getLyrics(); + QString vocalLanguage = dialog.getVocalLanguage(); + + SongItem newSong(caption, lyrics); + newSong.vocalLanguage = vocalLanguage; songModel->addSong(newSong); // Select the new item @@ -341,18 +302,17 @@ void MainWindow::onAddSongButtonClicked() } } -void MainWindow::onSongListViewDoubleClicked(const QModelIndex &index) +void MainWindow::on_songListView_doubleClicked(const QModelIndex &index) { if (!index.isValid()) return; - disconnect(ui->songListView, &QTableView::doubleClicked, this, &MainWindow::onSongListViewDoubleClicked); + disconnect(ui->songListView, &QTableView::doubleClicked, this, &MainWindow::on_songListView_doubleClicked); int row = index.row(); if (index.column() == 0) { - isPaused = false; if (isPlaying) { audioPlayer->stop(); @@ -360,8 +320,8 @@ void MainWindow::onSongListViewDoubleClicked(const QModelIndex &index) else { isPlaying = true; + updateControls(); } - updateControls(); flushGenerationQueue(); ui->nowPlayingLabel->setText("Now Playing: Waiting for generation..."); @@ -372,16 +332,24 @@ void MainWindow::onSongListViewDoubleClicked(const QModelIndex &index) { SongItem song = songModel->getSong(row); - SongDialog dialog(this, song); + SongDialog dialog(this, song.caption, song.lyrics, song.vocalLanguage); if (dialog.exec() == QDialog::Accepted) - songModel->updateSong(songModel->index(row, 1), dialog.getSong()); + { + 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); + } } - connect(ui->songListView, &QTableView::doubleClicked, this, &MainWindow::onSongListViewDoubleClicked); + connect(ui->songListView, &QTableView::doubleClicked, this, &MainWindow::on_songListView_doubleClicked); } -void MainWindow::onRemoveSongButtonClicked() +void MainWindow::on_removeSongButton_clicked() { QModelIndex currentIndex = ui->songListView->currentIndex(); if (!currentIndex.isValid()) @@ -401,18 +369,17 @@ void MainWindow::onRemoveSongButtonClicked() } } -void MainWindow::onAdvancedSettingsButtonClicked() +void MainWindow::on_advancedSettingsButton_clicked() { 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) { @@ -427,20 +394,12 @@ void MainWindow::onAdvancedSettingsButtonClicked() // 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(); } } @@ -453,19 +412,11 @@ void MainWindow::playbackStarted() void MainWindow::playSong(const SongItem& song) { currentSong = song; - if (song.audioData) - { - audioPlayer->play(song.audioData); - } - else if (!song.file.isEmpty()) - { - audioPlayer->play(song.file); - } + 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) @@ -551,7 +502,7 @@ void MainWindow::updatePlaybackStatus(bool playing) updateControls(); } -void MainWindow::onPositionSliderSliderMoved(int position) +void MainWindow::on_positionSlider_sliderMoved(int position) { if (isPlaying && audioPlayer->isPlaying()) { @@ -559,11 +510,6 @@ void MainWindow::onPositionSliderSliderMoved(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 @@ -574,7 +520,7 @@ void MainWindow::ensureSongsInQueue(bool enqeueCurrent) SongItem lastSong; SongItem workerSong; - if(aceStep->isGenerating(&workerSong)) + if(aceStep->isGenerateing(&workerSong)) lastSong = workerSong; else if(!generatedSongQueue.empty()) lastSong = generatedSongQueue.last(); @@ -595,18 +541,21 @@ void MainWindow::ensureSongsInQueue(bool enqeueCurrent) isGeneratingNext = true; ui->statusbar->showMessage("Generateing: "+nextSong.caption); - aceStep->requestGeneration(nextSong, jsonTemplate); + aceStep->requestGeneration(nextSong, jsonTemplate, + aceStepPath, qwen3ModelPath, + textEncoderModelPath, ditModelPath, + vaeModelPath); } void MainWindow::flushGenerationQueue() { generatedSongQueue.clear(); - aceStep->cancelGeneration(); + aceStep->cancleGenerateion(); isGeneratingNext = false; } // Playlist save/load methods -void MainWindow::onActionSavePlaylist() +void MainWindow::on_actionSavePlaylist() { QString filePath = QFileDialog::getSaveFileName(this, "Save Playlist", QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/playlist.json", @@ -618,7 +567,7 @@ void MainWindow::onActionSavePlaylist() } } -void MainWindow::onActionLoadPlaylist() +void MainWindow::on_actionLoadPlaylist() { QString filePath = QFileDialog::getOpenFileName(this, "Load Playlist", QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation), @@ -631,7 +580,7 @@ void MainWindow::onActionLoadPlaylist() } } -void MainWindow::onActionAppendPlaylist() +void MainWindow::on_actionAppendPlaylist() { QString filePath = QFileDialog::getOpenFileName(this, "Load Playlist", QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation), @@ -642,7 +591,7 @@ void MainWindow::onActionAppendPlaylist() } } -void MainWindow::onActionSaveSong() +void MainWindow::on_actionSaveSong() { QString filePath = QFileDialog::getSaveFileName(this, "Save Playlist", QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation) + "/song.json", @@ -666,22 +615,7 @@ void MainWindow::onActionSaveSong() QFile file(filePath); if (!file.open(QIODevice::WriteOnly | QIODevice::Text)) return; - - // 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"); - } - + QFile::copy(currentSong.file, filePath + ".wav"); file.write(jsonData); file.close(); } @@ -758,13 +692,16 @@ bool MainWindow::savePlaylistToJson(const QString &filePath, const QList(song.uniqueId); // Store as qint64 for JSON compatibility songsArray.append(songObj); } QJsonObject rootObj; rootObj["songs"] = songsArray; - rootObj["version"] = "1.1"; + rootObj["version"] = "1.0"; QJsonDocument doc(rootObj); QByteArray jsonData = doc.toJson(); @@ -813,7 +750,8 @@ bool MainWindow::loadPlaylistFromJson(const QString &filePath, QList & QJsonObject rootObj = doc.object(); - if (rootObj.contains("version") && rootObj["version"].toString() != "1.0" && rootObj["version"].toString() != "1.1") + // Check for version compatibility + if (rootObj.contains("version") && rootObj["version"].toString() != "1.0") { qWarning() << "Unsupported playlist version:" << rootObj["version"].toString(); return false; @@ -835,7 +773,35 @@ bool MainWindow::loadPlaylistFromJson(const QString &filePath, QList & continue; QJsonObject songObj = value.toObject(); - SongItem song(songObj); + 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(songObj["uniqueId"].toInteger()); + } + else + { + // Generate new ID for old playlists without uniqueId + song.uniqueId = QRandomGenerator::global()->generate64(); + } + songs.append(song); } diff --git a/src/MainWindow.h b/src/MainWindow.h index 25cdf9d..54136f7 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -1,8 +1,3 @@ -/* - * Copyright Carl Philipp Klemm 2026 - * SPDX-License-Identifier: GPL-3.0-or-later - */ - #ifndef MAINWINDOW_H #define MAINWINDOW_H @@ -36,7 +31,7 @@ class MainWindow : public QMainWindow SongListModel *songModel; AudioPlayer *audioPlayer; QThread aceThread; - AceStepWorker *aceStep; + AceStep *aceStep; QTimer *playbackTimer; QString formatTime(int milliseconds); @@ -50,6 +45,7 @@ class MainWindow : public QMainWindow QString jsonTemplate; // Path settings + QString aceStepPath; QString qwen3ModelPath; QString textEncoderModelPath; QString ditModelPath; @@ -67,20 +63,19 @@ public slots: void show(); private slots: - void onPlayButtonClicked(); - void onPauseButtonClicked(); - void onSkipButtonClicked(); - void onStopButtonClicked(); - void onShuffleButtonClicked(); - void onPositionSliderSliderMoved(int position); - void onVolumeSliderValueChanged(int value); + 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 updatePosition(int position); void updateDuration(int duration); - void onAddSongButtonClicked(); - void onRemoveSongButtonClicked(); - void onAdvancedSettingsButtonClicked(); + void on_addSongButton_clicked(); + void on_removeSongButton_clicked(); + void on_advancedSettingsButton_clicked(); - void onSongListViewDoubleClicked(const QModelIndex &index); + void on_songListView_doubleClicked(const QModelIndex &index); void songGenerated(const SongItem& song); void generationCanceld(const SongItem& song); @@ -89,10 +84,10 @@ private slots: void updatePlaybackStatus(bool playing); void generationError(const QString &error); - void onActionSavePlaylist(); - void onActionLoadPlaylist(); - void onActionAppendPlaylist(); - void onActionSaveSong(); + void on_actionSavePlaylist(); + void on_actionLoadPlaylist(); + void on_actionAppendPlaylist(); + void on_actionSaveSong(); private: void loadSettings(); diff --git a/src/MainWindow.ui b/src/MainWindow.ui index 37a6f78..828189b 100644 --- a/src/MainWindow.ui +++ b/src/MainWindow.ui @@ -178,9 +178,6 @@ - - false - Pause @@ -222,56 +219,14 @@ - - - - Qt::Orientation::Horizontal - - - - 15 - 20 - - - - - - - - Volume: - - - - - - - - 100 - 20 - - - - 100 - - - 50 - - - Qt::Orientation::Horizontal - - - Qt::Orientation::Horizontal - - QSizePolicy::Policy::Fixed - - 35 + 40 20 diff --git a/src/SongDialog.cpp b/src/SongDialog.cpp index e0ebde8..baa9ddc 100644 --- a/src/SongDialog.cpp +++ b/src/SongDialog.cpp @@ -1,142 +1,48 @@ -// Copyright Carl Philipp Klemm 2026 -// SPDX-License-Identifier: GPL-3.0-or-later - #include "SongDialog.h" #include "ui_SongDialog.h" #include -SongDialog::SongDialog(QWidget *parent, const SongItem &song) +SongDialog::SongDialog(QWidget *parent, const QString &caption, const QString &lyrics, const QString &vocalLanguage) : QDialog(parent), - song(song), - ui(new Ui::SongDialog) + ui(new Ui::SongDialog) { ui->setupUi(this); - // 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); + // Set initial values if provided + if (!caption.isEmpty()) + { + ui->captionEdit->setPlainText(caption); + } + if (!lyrics.isEmpty()) + { + ui->lyricsEdit->setPlainText(lyrics); + } // Setup vocal language combo box - ui->vocalLanguageCombo->addItem("--", ""); + ui->vocalLanguageCombo->addItem("--", ""); // Unset ui->vocalLanguageCombo->addItem("English (en)", "en"); - ui->vocalLanguageCombo->addItem("Chinese (zh)", "zh"); - 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("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("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"); - 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()) + // Set current language if provided + if (!vocalLanguage.isEmpty()) { - int index = ui->vocalLanguageCombo->findData(song.vocalLanguage); + int index = ui->vocalLanguageCombo->findData(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); + ui->vocalLanguageCombo->setCurrentIndex(0); // Default to unset } - - 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() @@ -144,10 +50,25 @@ SongDialog::~SongDialog() delete ui; } -void SongDialog::onOkButtonClicked() +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() { // Validate that caption is not empty - QString caption = ui->captionEdit->toPlainText(); + QString caption = getCaption(); if (caption.trimmed().isEmpty()) { QMessageBox::warning(this, "Invalid Input", "Caption cannot be empty."); @@ -157,22 +78,7 @@ void SongDialog::onOkButtonClicked() accept(); } -void SongDialog::onCancelButtonClicked() +void SongDialog::on_cancelButton_clicked() { 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; -} - +} \ No newline at end of file diff --git a/src/SongDialog.h b/src/SongDialog.h index 5e4ce48..c8137f5 100644 --- a/src/SongDialog.h +++ b/src/SongDialog.h @@ -1,16 +1,9 @@ -/* - * Copyright Carl Philipp Klemm 2026 - * SPDX-License-Identifier: GPL-3.0-or-later - */ - #ifndef SONGDIALOG_H #define SONGDIALOG_H #include #include -#include "SongItem.h" - namespace Ui { class SongDialog; @@ -19,17 +12,19 @@ class SongDialog; class SongDialog : public QDialog { Q_OBJECT - SongItem song; public: - explicit SongDialog(QWidget *parent = nullptr, const SongItem& song = SongItem()); + explicit SongDialog(QWidget *parent = nullptr, const QString &caption = "", const QString &lyrics = "", + const QString &vocalLanguage = ""); ~SongDialog(); - const SongItem& getSong(); + QString getCaption() const; + QString getLyrics() const; + QString getVocalLanguage() const; private slots: - void onOkButtonClicked(); - void onCancelButtonClicked(); + void on_okButton_clicked(); + void on_cancelButton_clicked(); private: Ui::SongDialog *ui; diff --git a/src/SongDialog.ui b/src/SongDialog.ui index 56e0842..efedda8 100644 --- a/src/SongDialog.ui +++ b/src/SongDialog.ui @@ -1,157 +1,109 @@ - - SongDialog - - - - 0 - 0 - 500 - 410 - - - - Song Details - - - - - - 0 - - - - - Caption: - - - Qt::AlignmentFlag::AlignTop - - - - - - - Enhance Caption - - - true - - - - - - - - - - - - Lyrics (optional): - - - Qt::AlignmentFlag::AlignTop - - - - - - - - - - 5 - - - - - Keyscale - - - - - - - true - - - - - - - Vocal Language: - - - Qt::AlignmentFlag::AlignLeading|Qt::AlignmentFlag::AlignLeft|Qt::AlignmentFlag::AlignVCenter - - - - - - - true - - - - - - - Bpm (0 for llm chooses) - - - - - - - 9999 - - - - - - - - - 5 - - - 0 - - - - - - - - - Qt::Orientation::Horizontal - - - - 40 - 20 - - - - - - - - OK - - - - - - - Cancel - - - - - - - - - - + + SongDialog + + + + 0 + 0 + 500 + 400 + + + + Song Details + + + + + + Caption: + + + Qt::AlignTop + + + + + + + Enter song caption (e.g., "Upbeat pop rock anthem with driving electric guitars") + + + 80 + + + + + + + Lyrics (optional): + + + Qt::AlignTop + + + + + + + Enter lyrics or leave empty for instrumental music + + + 150 + + + + + + + Vocal Language: + + + Qt::AlignTop + + + + + + + true + + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + OK + + + + + + + Cancel + + + + + + + + + + \ No newline at end of file diff --git a/src/SongItem.cpp b/src/SongItem.cpp deleted file mode 100644 index 4b1686e..0000000 --- a/src/SongItem.cpp +++ /dev/null @@ -1,43 +0,0 @@ -#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(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); -} - diff --git a/src/SongItem.h b/src/SongItem.h index 0a80926..8a28891 100644 --- a/src/SongItem.h +++ b/src/SongItem.h @@ -1,33 +1,22 @@ -/* - * Copyright Carl Philipp Klemm 2026 - * SPDX-License-Identifier: GPL-3.0-or-later - */ - #pragma once #include #include #include -#include -#include 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 audioData; - SongItem(const QString &caption = "", const QString &lyrics = ""); - SongItem(const QJsonObject& json); - - void store(QJsonObject& json) const; - void load(const QJsonObject& json); + 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(); + } }; diff --git a/src/SongListModel.cpp b/src/SongListModel.cpp index 0b40f05..caa856a 100644 --- a/src/SongListModel.cpp +++ b/src/SongListModel.cpp @@ -1,6 +1,3 @@ -// Copyright Carl Philipp Klemm 2026 -// SPDX-License-Identifier: GPL-3.0-or-later - #include "SongListModel.h" #include #include @@ -111,26 +108,6 @@ 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()) diff --git a/src/SongListModel.h b/src/SongListModel.h index bf1e22e..4a3275d 100644 --- a/src/SongListModel.h +++ b/src/SongListModel.h @@ -1,8 +1,3 @@ -/* - * Copyright Carl Philipp Klemm 2026 - * SPDX-License-Identifier: GPL-3.0-or-later - */ - #ifndef SONGLISTMODEL_H #define SONGLISTMODEL_H @@ -37,7 +32,6 @@ 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 diff --git a/src/clickableslider.cpp b/src/clickableslider.cpp index 63477aa..3006b2e 100644 --- a/src/clickableslider.cpp +++ b/src/clickableslider.cpp @@ -1,6 +1,3 @@ -// Copyright Carl Philipp Klemm 2026 -// SPDX-License-Identifier: GPL-3.0-or-later - #include "clickableslider.h" #include diff --git a/src/clickableslider.h b/src/clickableslider.h index 0d651e1..4b4e669 100644 --- a/src/clickableslider.h +++ b/src/clickableslider.h @@ -1,8 +1,3 @@ -/* - * Copyright Carl Philipp Klemm 2026 - * SPDX-License-Identifier: GPL-3.0-or-later - */ - #ifndef CLICKABLESLIDER_H #define CLICKABLESLIDER_H diff --git a/src/elidedlabel.cpp b/src/elidedlabel.cpp index 5830ed7..191a0fd 100644 --- a/src/elidedlabel.cpp +++ b/src/elidedlabel.cpp @@ -1,6 +1,3 @@ -// Copyright Carl Philipp Klemm 2026 -// SPDX-License-Identifier: GPL-3.0-or-later - #include "elidedlabel.h" #include diff --git a/src/elidedlabel.h b/src/elidedlabel.h index 285a41c..76df007 100644 --- a/src/elidedlabel.h +++ b/src/elidedlabel.h @@ -1,8 +1,3 @@ -/* - * Copyright Carl Philipp Klemm 2026 - * SPDX-License-Identifier: GPL-3.0-or-later - */ - #ifndef ELIDEDLABEL_H #define ELIDEDLABEL_H diff --git a/src/main.cpp b/src/main.cpp index f401fda..c51ac1f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,6 +1,3 @@ -// Copyright Carl Philipp Klemm 2026 -// SPDX-License-Identifier: GPL-3.0-or-later - #include #include diff --git a/tests/test_acestep_worker.cpp b/tests/test_acestep_worker.cpp deleted file mode 100644 index 60a5ffa..0000000 --- a/tests/test_acestep_worker.cpp +++ /dev/null @@ -1,525 +0,0 @@ -// Test for AceStepWorker -// Compile with: cmake .. && make test_acestep_worker && ./test_acestep_worker - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#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; -} \ No newline at end of file diff --git a/third_party/acestep.cpp b/third_party/acestep.cpp deleted file mode 160000 index d28398d..0000000 --- a/third_party/acestep.cpp +++ /dev/null @@ -1 +0,0 @@ -Subproject commit d28398db0ffdb77e8ae071ff31bde8c559e7085a