From 533ea2d3ed850782782c8f13d794042b156be68b Mon Sep 17 00:00:00 2001 From: Dmitry Makarenko Date: Fri, 2 May 2025 12:40:20 +0300 Subject: [PATCH] Enable ARM build --- .github/workflows/build.yml | 50 ++ .gitignore | 14 + installer/Resources/Conclusion.html | 31 + installer/Resources/License.html | 31 + installer/Resources/ReadMe.html | 35 ++ installer/Resources/Welcome.html | 33 + installer/Scripts/postinstall | 74 +++ installer/distribution.xml | 27 + patches/audacity/modules.patch | 10 + .../htdemux.h.patch | 28 + .../musicgen.patch | 11 + .../openvino-plugins-ai-audacity/paths.patch | 105 ++++ scripts/build-arm64.sh | 179 ++++++ scripts/postinstall | 67 ++ scripts/prepare-build.sh | 58 ++ .../project.pbxproj | 538 ++++++++++++++++ .../contents.xcworkspacedata | 7 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 58 ++ .../Assets.xcassets/Contents.json | 6 + .../ResourceDownloader.entitlements | 12 + .../ResourceDownloaderApp.swift | 591 ++++++++++++++++++ .../ResourceDownloader/models.json | 256 ++++++++ .../ResourceDownloaderTests.swift | 10 + .../ResourceDownloaderUITests.swift | 34 + ...ResourceDownloaderUITestsLaunchTests.swift | 26 + 26 files changed, 2302 insertions(+) create mode 100644 .github/workflows/build.yml create mode 100644 installer/Resources/Conclusion.html create mode 100644 installer/Resources/License.html create mode 100644 installer/Resources/ReadMe.html create mode 100644 installer/Resources/Welcome.html create mode 100755 installer/Scripts/postinstall create mode 100644 installer/distribution.xml create mode 100644 patches/audacity/modules.patch create mode 100644 patches/openvino-plugins-ai-audacity/htdemux.h.patch create mode 100644 patches/openvino-plugins-ai-audacity/musicgen.patch create mode 100644 patches/openvino-plugins-ai-audacity/paths.patch create mode 100755 scripts/build-arm64.sh create mode 100755 scripts/postinstall create mode 100755 scripts/prepare-build.sh create mode 100644 tools/ResourceDownloader/ResourceDownloader.xcodeproj/project.pbxproj create mode 100644 tools/ResourceDownloader/ResourceDownloader.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 tools/ResourceDownloader/ResourceDownloader/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 tools/ResourceDownloader/ResourceDownloader/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 tools/ResourceDownloader/ResourceDownloader/Assets.xcassets/Contents.json create mode 100644 tools/ResourceDownloader/ResourceDownloader/ResourceDownloader.entitlements create mode 100644 tools/ResourceDownloader/ResourceDownloader/ResourceDownloaderApp.swift create mode 100644 tools/ResourceDownloader/ResourceDownloader/models.json create mode 100644 tools/ResourceDownloader/ResourceDownloaderTests/ResourceDownloaderTests.swift create mode 100644 tools/ResourceDownloader/ResourceDownloaderUITests/ResourceDownloaderUITests.swift create mode 100644 tools/ResourceDownloader/ResourceDownloaderUITests/ResourceDownloaderUITestsLaunchTests.swift diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..6cb67b0 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,50 @@ +name: build + +on: + push: + branches: + - main + + pull_request: + + workflow_dispatch: + +env: + ARTIFACT_PATH: ${{ github.workspace }}/artifact + BUILD_PATH: ${{ github.workspace }}/build + PACKAGE_PATH: ${{ github.workspace }}/package + SOURCE_PATH: ${{ github.workspace }}/sources + STAGING_PATH: ${{ github.workspace }}/staging + +jobs: + build-openvino-plugins-arm64: + runs-on: macos-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + path: ${{ github.workspace }} + + - name: Install Apple codesigning certificates + uses: apple-actions/import-codesign-certs@v2 + env: + P12_FILE_BASE64: ${{ secrets.APPLE_CERTIFICATE }} + P12_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + if: ${{ env.P12_FILE_BASE64 != '' && env.P12_PASSWORD != '' }} + with: + p12-file-base64: ${{ secrets.APPLE_CERTIFICATE }} + p12-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} + + - name: Build audacity for arm64 + run: | + scripts/prepare-build.sh + scripts/build-arm64.sh + env: + APPLE_CODESIGN_IDENTITY: ${{ secrets.APPLE_CODESIGN_IDENTITY }} + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: mod-openvino-arm64 + path: ${{ env.STAGING_PATH }}/Audacity-OpenVINO.pkg + diff --git a/.gitignore b/.gitignore index 6c6be34..7bdbf58 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,19 @@ build/* sources/* staging/* +packages/* *.pkg +.DS_Store + +### Xcode ### +xcuserdata/ +*.xcscmblueprint +*.xccheckout +*.xcodeproj/* +!*.xcodeproj/project.pbxproj +!*.xcodeproj/xcshareddata/ +!*.xcodeproj/project.xcworkspace/ +!*.xcworkspace/contents.xcworkspacedata +/*.gcno +**/xcshareddata/WorkspaceSettings.xcsettings \ No newline at end of file diff --git a/installer/Resources/Conclusion.html b/installer/Resources/Conclusion.html new file mode 100644 index 0000000..06f592d --- /dev/null +++ b/installer/Resources/Conclusion.html @@ -0,0 +1,31 @@ + + +Installation Complete + + + +

Installation Complete

+

The Audacity OpenVINO Plugin has been successfully installed.

+

Launch Audacity to start using the new features!

+ + diff --git a/installer/Resources/License.html b/installer/Resources/License.html new file mode 100644 index 0000000..3de2c66 --- /dev/null +++ b/installer/Resources/License.html @@ -0,0 +1,31 @@ + + +License Agreement + + + +

License Agreement

+

This software is provided under the GNU General Public License v3 (GPLv3).

+

Please review the full license terms before proceeding.

+ + diff --git a/installer/Resources/ReadMe.html b/installer/Resources/ReadMe.html new file mode 100644 index 0000000..b4c2efc --- /dev/null +++ b/installer/Resources/ReadMe.html @@ -0,0 +1,35 @@ + + +Important + + + +

Please read

+ + + diff --git a/installer/Resources/Welcome.html b/installer/Resources/Welcome.html new file mode 100644 index 0000000..db41e1c --- /dev/null +++ b/installer/Resources/Welcome.html @@ -0,0 +1,33 @@ + + +Welcome to Audacity OpenVINO Plugin + + + +

Audacity OpenVINO Plugin

+

This plugin adds AI-based features to Audacity using Intel's OpenVINO toolkit.

+

Note: During installation, the plugin will download additional AI models.

+

These downloads can be large and may take a significant amount of time depending on your internet speed and model selection.

+

The installer will appear to "hang" on "Running package scripts" during this process β€” this is expected.

+ + diff --git a/installer/Scripts/postinstall b/installer/Scripts/postinstall new file mode 100755 index 0000000..ced3ffc --- /dev/null +++ b/installer/Scripts/postinstall @@ -0,0 +1,74 @@ +#!/bin/bash + +# This script runs after the installation +# It updates the Audacity configuration file to enable the OpenVINO module + +MODULE_NAME="mod-openvino" +MODULE_PATH="/Library/Application Support/audacity/modules/${MODULE_NAME}.so" +MODULE_DATETIME=$(stat -f "%m" "$MODULE_PATH" | xargs -I{} date -r {} +"%Y-%m-%dT%H:%M:%S") + +SCRIPT_DIR="$(dirname "$0")" +APP_PATH="$SCRIPT_DIR/ResourceDownloader.app" +LOGGED_IN_USER=$(stat -f "%Su" /dev/console) + +# Block until the app quits +sudo -u "$LOGGED_IN_USER" open -W "$APP_PATH" + +update_config_section() { + + local cfg_file="$1" + local section="$2" + local key="$3" + local value="$4" + local tmp_file="${cfg_file}.tmp" + original_owner=$(stat -f "%u" "$cfg_file") + original_group=$(stat -f "%g" "$cfg_file") + + awk -v section="$section" -v key="$key" -v value="$value" ' + BEGIN { + found_section = 0 + found_key = 0 + } + $0 == "[" section "]" { + found_section = 1 + print + next + } + found_section && $0 ~ "^" key{ + print key "=" value + found_key = 1 + found_section = 0 + next + } + found_section && !found_key && /^\[.*\]/ { + print key "=" value + found_key = 1 + found_section = 0 + } + { print } + END { } + ' "$cfg_file" > "$tmp_file" && mv "$tmp_file" "$cfg_file" + chown "$original_owner":"$original_group" "$cfg_file" +} + +# Loop through all user home directories in /Users (excluding system users) +for USER_HOME in /Users/*; do + # Only act on real user dirs + [ -d "$USER_HOME" ] || continue + [ -f "$USER_HOME/.zshrc" ] || [ -f "$USER_HOME/.bash_profile" ] || continue + + CFG_PATH="$USER_HOME/Library/Application Support/audacity/audacity.cfg" + CFG_PATH1="$USER_HOME/Library/Application Support/audacity/_audacity.cfg" + + # Skip if the config doesn't exist + [ -f "$CFG_PATH" ] || continue + + echo "Updating $CFG_PATH" + + update_config_section "$CFG_PATH" "Module" "mod-openvino" "1" + update_config_section "$CFG_PATH" "ModuleDateTime" "mod-openvino" "$MODULE_DATETIME" + update_config_section "$CFG_PATH" "ModulePath" "mod-openvino" "$MODULE_PATH" + +done + +exit 0 diff --git a/installer/distribution.xml b/installer/distribution.xml new file mode 100644 index 0000000..1b3e535 --- /dev/null +++ b/installer/distribution.xml @@ -0,0 +1,27 @@ + + + Audacity OpenVINO module + + + + + + + + + + + + + + + + + + #openvino-module.pkg + + + + + + diff --git a/patches/audacity/modules.patch b/patches/audacity/modules.patch new file mode 100644 index 0000000..033e2be --- /dev/null +++ b/patches/audacity/modules.patch @@ -0,0 +1,10 @@ +--- modules/CMakeLists.txt.orig 2025-04-24 14:23:00.161466283 +0300 ++++ modules/CMakeLists.txt 2025-04-24 14:23:19.002006483 +0300 +@@ -9,6 +9,7 @@ + # The list of module sub-folders is ordered so that each folder occurs after any + # others that it depends on + set( FOLDERS ++ mod-openvino + etc + import-export + track-ui diff --git a/patches/openvino-plugins-ai-audacity/htdemux.h.patch b/patches/openvino-plugins-ai-audacity/htdemux.h.patch new file mode 100644 index 0000000..12ee894 --- /dev/null +++ b/patches/openvino-plugins-ai-audacity/htdemux.h.patch @@ -0,0 +1,28 @@ +--- mod-openvino/htdemucs.h.orig 2024-12-20 16:59:14.000000000 +0300 ++++ mod-openvino/htdemucs.h 2025-04-24 14:58:08.662787586 +0300 +@@ -4,7 +4,7 @@ + #include + #include + +-namespace torch ++namespace at + { + class Tensor; + } +@@ -41,11 +41,11 @@ + std::shared_ptr< HTDemucs_impl > _impl; + #endif + std::shared_ptr< HTDemucs_openvino_impl > _impl_ov; +- bool _apply_model_0(torch::Tensor& mix, torch::Tensor& out, int64_t shifts = 1, bool split = true, double overlap = 0.25, double transition_power = 1., int64_t static_shifts = 1); +- bool _apply_model_1(torch::Tensor& mix, torch::Tensor& out, int64_t shifts = 1, bool split = true, double overlap = 0.25, double transition_power = 1., int64_t static_shifts = 1); +- bool _apply_model_2(TensorChunk& mix, torch::Tensor& out, bool split = true, double overlap = 0.25, double transition_power = 1., int64_t static_shifts = 1); +- bool _apply_model_3(TensorChunk& mix, torch::Tensor& out); +- bool _actually_run_model(torch::Tensor& mix_tensor, torch::Tensor& x); ++ bool _apply_model_0(at::Tensor& mix, at::Tensor& out, int64_t shifts = 1, bool split = true, double overlap = 0.25, double transition_power = 1., int64_t static_shifts = 1); ++ bool _apply_model_1(at::Tensor& mix, at::Tensor& out, int64_t shifts = 1, bool split = true, double overlap = 0.25, double transition_power = 1., int64_t static_shifts = 1); ++ bool _apply_model_2(TensorChunk& mix, at::Tensor& out, bool split = true, double overlap = 0.25, double transition_power = 1., int64_t static_shifts = 1); ++ bool _apply_model_3(TensorChunk& mix, at::Tensor& out); ++ bool _actually_run_model(at::Tensor& mix_tensor, at::Tensor& x); + + int64_t _shifts = 0; + int64_t _offsets = 0; diff --git a/patches/openvino-plugins-ai-audacity/musicgen.patch b/patches/openvino-plugins-ai-audacity/musicgen.patch new file mode 100644 index 0000000..07d4a44 --- /dev/null +++ b/patches/openvino-plugins-ai-audacity/musicgen.patch @@ -0,0 +1,11 @@ +--- mod-openvino/musicgen/music_gen_decoder_cl.cpp.orig 2025-04-24 14:58:41.839354735 +0300 ++++ mod-openvino/musicgen/music_gen_decoder_cl.cpp 2025-04-24 15:00:48.024587074 +0300 +@@ -521,7 +521,7 @@ + { + //slice the new key values into the existing past_key_vals buffer using OpenCL. + std::array srcOrigin = { 0, 0, 0 }; // Start at the beginning of the source buffer +- std::array dstOrigin = { 0, _past_length, 0 }; ++ std::array dstOrigin = { 0, (unsigned long) _past_length, 0 }; + + // Size of one element + std::array region = { sizeof(ov::float16) * past_key_values_shape[3], 1, past_key_values_shape[0] * past_key_values_shape[1] }; diff --git a/patches/openvino-plugins-ai-audacity/paths.patch b/patches/openvino-plugins-ai-audacity/paths.patch new file mode 100644 index 0000000..7e3aaf7 --- /dev/null +++ b/patches/openvino-plugins-ai-audacity/paths.patch @@ -0,0 +1,105 @@ +diff --color -ruN mod-openvino.orig/OVAudioSR.cpp mod-openvino/OVAudioSR.cpp +--- mod-openvino.orig/OVAudioSR.cpp 2025-04-26 15:56:46.861144303 +0300 ++++ mod-openvino/OVAudioSR.cpp 2025-04-26 16:00:47.323405438 +0300 +@@ -52,7 +52,7 @@ + { + std::vector available_models; + +- auto model_folder = wxFileName(FileNames::BaseDir(), wxT("openvino-models")).GetFullPath(); ++ auto model_folder = wxFileName(FileNames::DataDir(), wxT("openvino-models")).GetFullPath(); + model_folder = wxFileName(model_folder, wxT("audiosr")).GetFullPath(); + + //make sure that all of the 'base' models are present +@@ -607,7 +607,7 @@ + //todo: Right now we're looking for the model in the 'BaseDir' (which is top-level folder of Audacity install) + // This might be okay, but some users may not have permissions to place models there. So, also look in + // DataDir(), which is the path to C:\Users\\AppData\Roaming\audacity. +- auto model_folder_wx = wxFileName(FileNames::BaseDir(), wxT("openvino-models")).GetFullPath(); ++ auto model_folder_wx = wxFileName(FileNames::DataDir(), wxT("openvino-models")).GetFullPath(); + auto model_folder = audacity::ToUTF8(wxFileName(model_folder_wx, wxT("audiosr")).GetFullPath()); + + FilePath cache_folder = FileNames::MkDir(wxFileName(FileNames::DataDir(), wxT("openvino-model-cache")).GetFullPath()); +diff --color -ruN mod-openvino.orig/OVMusicGenerationLLM.cpp mod-openvino/OVMusicGenerationLLM.cpp +--- mod-openvino.orig/OVMusicGenerationLLM.cpp 2025-04-26 15:56:46.862479864 +0300 ++++ mod-openvino/OVMusicGenerationLLM.cpp 2025-04-26 16:00:47.329323234 +0300 +@@ -70,7 +70,7 @@ + { + std::vector available_models; + +- auto model_folder = wxFileName(FileNames::BaseDir(), wxT("openvino-models")).GetFullPath(); ++ auto model_folder = wxFileName(FileNames::DataDir(), wxT("openvino-models")).GetFullPath(); + model_folder = wxFileName(model_folder, wxT("musicgen")).GetFullPath(); + + //make sure that a couple of the 'base' models, like EnCodec, tokenizer are present. +@@ -301,7 +301,7 @@ + bool bGoodResult = true; + + { +- FilePath model_folder = FileNames::MkDir(wxFileName(FileNames::BaseDir(), wxT("openvino-models")).GetFullPath()); ++ FilePath model_folder = FileNames::MkDir(wxFileName(FileNames::DataDir(), wxT("openvino-models")).GetFullPath()); + std::string musicgen_model_folder = audacity::ToUTF8(wxFileName(model_folder, wxString("musicgen")) + .GetFullPath()); + +diff --color -ruN mod-openvino.orig/OVMusicSeparation.cpp mod-openvino/OVMusicSeparation.cpp +--- mod-openvino.orig/OVMusicSeparation.cpp 2025-04-26 15:56:46.863791883 +0300 ++++ mod-openvino/OVMusicSeparation.cpp 2025-04-26 16:00:47.323431730 +0300 +@@ -254,7 +254,7 @@ + //todo: Right now we're looking for the model in the 'BaseDir' (which is top-level folder of Audacity install) + // This might be okay, but some users may not have permissions to place models there. So, also look in + // DataDir(), which is the path to C:\Users\\AppData\Roaming\audacity. +- FilePath model_folder = FileNames::MkDir(wxFileName(FileNames::BaseDir(), wxT("openvino-models")).GetFullPath()); ++ FilePath model_folder = FileNames::MkDir(wxFileName(FileNames::DataDir(), wxT("openvino-models")).GetFullPath()); + std::string demucs_v4_path = audacity::ToUTF8(wxFileName(model_folder, wxT("htdemucs_v4.xml")) + .GetFullPath()); + +diff --color -ruN mod-openvino.orig/OVNoiseSuppression.cpp mod-openvino/OVNoiseSuppression.cpp +--- mod-openvino.orig/OVNoiseSuppression.cpp 2025-04-26 15:56:46.865271447 +0300 ++++ mod-openvino/OVNoiseSuppression.cpp 2025-04-26 16:00:47.329279025 +0300 +@@ -50,7 +50,7 @@ + + static bool is_deepfilter_model_present(std::string deepfilter_basename) + { +- auto model_folder = wxFileName(FileNames::BaseDir(), wxT("openvino-models")).GetFullPath(); ++ auto model_folder = wxFileName(FileNames::DataDir(), wxT("openvino-models")).GetFullPath(); + model_folder = wxFileName(model_folder, wxString(deepfilter_basename)).GetFullPath(); + + std::vector< std::string > model_basenames = { "enc", "erb_dec", "df_dec" }; +@@ -77,7 +77,7 @@ + + static bool is_omz_model_present(std::string omz_model_basename) + { +- auto model_folder = wxFileName(FileNames::BaseDir(), wxT("openvino-models")).GetFullPath(); ++ auto model_folder = wxFileName(FileNames::DataDir(), wxT("openvino-models")).GetFullPath(); + + auto binmodelpath = wxFileName(model_folder, wxString(omz_model_basename + ".bin")); + auto xmlmodelpath = wxFileName(model_folder, wxString(omz_model_basename + ".xml")); +@@ -384,7 +384,7 @@ + try + { + //CompileNoiseSuppression(compiledModel); +- FilePath model_folder = FileNames::MkDir(wxFileName(FileNames::BaseDir(), wxT("openvino-models")).GetFullPath()); ++ FilePath model_folder = FileNames::MkDir(wxFileName(FileNames::DataDir(), wxT("openvino-models")).GetFullPath()); + FilePath cache_folder = FileNames::MkDir(wxFileName(FileNames::DataDir(), wxT("openvino-model-cache")).GetFullPath()); + std::string cache_path = wstring_to_string(wxFileName(cache_folder).GetFullPath().ToStdWstring()); + +diff --color -ruN mod-openvino.orig/OVWhisperTranscription.cpp mod-openvino/OVWhisperTranscription.cpp +--- mod-openvino.orig/OVWhisperTranscription.cpp 2025-04-26 15:56:46.867815026 +0300 ++++ mod-openvino/OVWhisperTranscription.cpp 2025-04-26 16:00:47.323457689 +0300 +@@ -292,7 +292,7 @@ + static bool is_whisper_model_present(std::string whisper_basename) + { + std::cout << "is_whisper_model_present(" << whisper_basename << ")" << std::endl; +- auto model_folder = wxFileName(FileNames::BaseDir(), wxT("openvino-models")).GetFullPath(); ++ auto model_folder = wxFileName(FileNames::DataDir(), wxT("openvino-models")).GetFullPath(); + + { + std::string ggml_binname = std::string("ggml-") + whisper_basename + std::string(".bin"); +@@ -877,7 +877,7 @@ + params.prompt = mInitialPrompt; + + //whisper init +- FilePath model_folder = FileNames::MkDir(wxFileName(FileNames::BaseDir(), wxT("openvino-models")).GetFullPath()); ++ FilePath model_folder = FileNames::MkDir(wxFileName(FileNames::DataDir(), wxT("openvino-models")).GetFullPath()); + std::string whisper_variant = mSupportedModels[m_modelSelectionChoice]; + std::string ggml_binname = std::string("ggml-") + whisper_variant + std::string(".bin"); + std::string whisper_model_path = audacity::ToUTF8(wxFileName(model_folder, wxString(ggml_binname)) diff --git a/scripts/build-arm64.sh b/scripts/build-arm64.sh new file mode 100755 index 0000000..07a5ff9 --- /dev/null +++ b/scripts/build-arm64.sh @@ -0,0 +1,179 @@ +#!/bin/bash + +set -e +set -x +set -o pipefail + +brew install opencl-clhpp-headers +brew install libomp + +MODULE_VERSION="3.7.1-R4.2" +ROOT_DIR=$(pwd) +SOURCE_PATH=$(pwd)/sources +PACKAGE_PATH=$(pwd)/packages +BUILD_PATH=$(pwd)/build +STAGING_PATH=$(pwd)/staging + +echo "Applying patches..." + +for repo in $(ls $SOURCE_PATH); do + patch_dir=${ROOT_DIR}/patches/$repo + if [ -d "$patch_dir" ]; then + echo "Applying patches to $repo" + for patch in $patch_dir/*.patch; do + if [ -f "$patch" ]; then + echo "Applying patch $patch" + patch -d $SOURCE_PATH/$repo < $patch + fi + done + else + echo "No patches found for $repo" + fi +done + +cp -r $SOURCE_PATH/openvino-plugins-ai-audacity/mod-openvino $SOURCE_PATH/audacity/modules + +cd $PACKAGE_PATH +wget https://storage.openvinotoolkit.org/repositories/openvino/packages/2024.0/macos/m_openvino_toolkit_macos_11_0_2024.0.0.14509.34caeefd078_arm64.tgz +tar xvf m_openvino_toolkit_macos_11_0_2024.0.0.14509.34caeefd078_arm64.tgz +source m_openvino_toolkit_macos_11_0_2024.0.0.14509.34caeefd078_arm64/setupvars.sh + +wget https://download.pytorch.org/libtorch/cpu/libtorch-macos-arm64-2.2.2.zip +unzip libtorch-macos-arm64-2.2.2.zip +export LIBTORCH_ROOTDIR=$PACKAGE_PATH/libtorch + +mkdir -p $BUILD_PATH/whisper +cd $BUILD_PATH/whisper +cmake $SOURCE_PATH/whisper.cpp -DWHISPER_OPENVINO=ON -DMACOS_ARCHITECTURE=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 -DWHISPER_NO_ACCELERATE=ON +make -j`sysctl -n hw.ncpu` + +cmake --install . --config Release --prefix $PACKAGE_PATH/whisper +export WHISPERCPP_ROOTDIR=$PACKAGE_PATH/whisper +export LD_LIBRARY_PATH=${WHISPERCPP_ROOTDIR}/lib:$LD_LIBRARY_PATH + +mkdir -p $BUILD_PATH/openvino_tokenizers +cd $BUILD_PATH/openvino_tokenizers +cmake $SOURCE_PATH/openvino_tokenizers -DMACOS_ARCHITECTURE=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=11.0 +make -j`sysctl -n hw.ncpu` + +cmake --install . --config Release --prefix $PACKAGE_PATH/openvino_tokenizers + +mkdir -p $BUILD_PATH/audacity +cd $BUILD_PATH/audacity + +cmake -G "Unix Makefiles" \ + -D CMAKE_CXX_FLAGS="-I/opt/homebrew/opt/opencl-clhpp-headers/include" \ + -DMACOS_ARCHITECTURE=arm64 -DCMAKE_OSX_DEPLOYMENT_TARGET=14.0\ + $SOURCE_PATH/audacity -DCMAKE_BUILD_TYPE=Release + +make -j`sysctl -n hw.ncpu` + +mkdir -p $STAGING_PATH +cd $STAGING_PATH +MODULE_PATH="$STAGING_PATH/dist/Library/Application Support/audacity/modules" + +mkdir -p "$MODULE_PATH/libs" + +cp -p $BUILD_PATH/audacity/Release/Audacity.app/Contents/modules/mod-openvino.so \ + "$MODULE_PATH" + +cp -p $PACKAGE_PATH/m_openvino_toolkit_macos_11_0_2024.0.0.14509.34caeefd078_arm64/runtime/lib/arm64/Release/*.so \ + "$MODULE_PATH/libs" +cp -p $PACKAGE_PATH/m_openvino_toolkit_macos_11_0_2024.0.0.14509.34caeefd078_arm64/runtime/lib/arm64/Release/*.dylib \ + "$MODULE_PATH/libs" +cp -p $PACKAGE_PATH/m_openvino_toolkit_macos_11_0_2024.0.0.14509.34caeefd078_arm64/runtime/3rdparty/tbb/lib/*.dylib \ + "$MODULE_PATH/libs" + +cp -p $PACKAGE_PATH/libtorch/lib/libc10.dylib \ + "$MODULE_PATH/libs" +cp -p $PACKAGE_PATH/libtorch/lib/libtorch.dylib \ + "$MODULE_PATH/libs" +cp -p $PACKAGE_PATH/libtorch/lib/libtorch_cpu.dylib \ + "$MODULE_PATH/libs" + +cp -P $PACKAGE_PATH/whisper/lib/*.dylib \ + "$MODULE_PATH/libs" + +cp -P $PACKAGE_PATH/openvino_tokenizers/lib/*.dylib \ + "$MODULE_PATH/libs/" + +cp /opt/homebrew/opt/libomp/lib/libomp.dylib \ + "$MODULE_PATH/libs/" + +chmod -R ug+w "$MODULE_PATH" + +xattr -cr "$MODULE_PATH" + +# Fix loading paths +cd "$MODULE_PATH" +for lib in *.so *.dylib; do + [ -e "$lib" ] || continue + + echo "Processing $lib..." + + deps=$(otool -L "$lib" | awk 'NR>1 {print $1}' | grep '@loader_path/../Frameworks/') + + for dep in $deps; do + + dep_filename=$(basename "$dep") + + # If we have this file in the libs directory, use it + if [[ -f "./libs/$dep_filename" ]]; then + new_dep="@rpath/$dep_filename" + echo " Updating dependency: $dep β†’ $new_dep" + install_name_tool -change "$dep" "$new_dep" "$lib" + else + # If the file does not exist load it from the Audacity/Frameworks directory + new_dep="@executable_path/../Frameworks/$dep_filename" + echo " Updating dependency: $dep β†’ $new_dep" + install_name_tool -change "$dep" "$new_dep" "$lib" + fi + done + + install_name_tool -add_rpath @loader_path/libs "$lib" +done + +cd $ROOT_DIR +cp -r installer/* "$STAGING_PATH" + +sudo xcode-select -s /Applications/Xcode_16.2.app + +PROJECT="tools/ResourceDownloader/ResourceDownloader.xcodeproj" +SCHEME="ResourceDownloader" +CONFIG="Release" +BUILD_DIR="build" + +xcodebuild \ + -project "$PROJECT" \ + -scheme "$SCHEME" \ + -configuration "$CONFIG" \ + -derivedDataPath "$BUILD_DIR" \ + clean build + +APP_PATH="$BUILD_DIR/Build/Products/$CONFIG/$SCHEME.app" + +# codesign --verbose --timestamp --identifier "org.audacityteam.resourcedownloader" --sign "${APPLE_CODESIGN_IDENTITY}" "$APP_PATH" + +cp -R "$APP_PATH" "$STAGING_PATH/Scripts" + +cd "$STAGING_PATH" +mkdir -p packages + +pkgbuild \ + --root dist \ + --scripts Scripts \ + --install-location / \ + --identifier org.audacityteam.audacity \ + --version "$MODULE_VERSION" \ + packages/openvino-module.pkg + +# productsign --sign "${APPLE_CODESIGN_IDENTITY}" packages/openvino-module.pkg packages/openvino-module.pkg + +productbuild --distribution distribution.xml \ + --resources Resources \ + --package-path ./packages \ + ./Audacity-OpenVINO.pkg + +# productsign --sign "${APPLE_CODESIGN_IDENTITY}" final.pkg final.pkg + +echo "Done." diff --git a/scripts/postinstall b/scripts/postinstall new file mode 100755 index 0000000..4ae6b86 --- /dev/null +++ b/scripts/postinstall @@ -0,0 +1,67 @@ +#!/bin/bash + +# This script runs after the installation +# It updates the Audacity configuration file to enable the OpenVINO module + +MODULE_NAME="mod-openvino" +MODULE_PATH="/Library/Application Support/audacity/modules/${MODULE_NAME}.so" +MODULE_DATETIME=$(stat -f "%m" "$MODULE_PATH" | xargs -I{} date -r {} +"%Y-%m-%dT%H:%M:%S") + +update_config_section() { + + local cfg_file="$1" + local section="$2" + local key="$3" + local value="$4" + local tmp_file="${cfg_file}.tmp" + original_owner=$(stat -f "%u" "$cfg_file") + original_group=$(stat -f "%g" "$cfg_file") + + awk -v section="$section" -v key="$key" -v value="$value" ' + BEGIN { + found_section = 0 + found_key = 0 + } + $0 == "[" section "]" { + found_section = 1 + print + next + } + found_section && $0 ~ "^" key{ + print key "=" value + found_key = 1 + found_section = 0 + next + } + found_section && !found_key && /^\[.*\]/ { + print key "=" value + found_key = 1 + found_section = 0 + } + { print } + END { } + ' "$cfg_file" > "$tmp_file" && mv "$tmp_file" "$cfg_file" + chown "$original_owner":"$original_group" "$cfg_file" +} + +# Loop through all user home directories in /Users (excluding system users) +for USER_HOME in /Users/*; do + # Only act on real user dirs + [ -d "$USER_HOME" ] || continue + [ -f "$USER_HOME/.zshrc" ] || [ -f "$USER_HOME/.bash_profile" ] || continue + + CFG_PATH="$USER_HOME/Library/Application Support/audacity/audacity.cfg" + CFG_PATH1="$USER_HOME/Library/Application Support/audacity/_audacity.cfg" + + # Skip if the config doesn't exist + [ -f "$CFG_PATH" ] || continue + + echo "Updating $CFG_PATH" + + update_config_section "$CFG_PATH" "Module" "mod-openvino" "1" + update_config_section "$CFG_PATH" "ModuleDateTime" "mod-openvino" "$MODULE_DATETIME" + update_config_section "$CFG_PATH" "ModulePath" "mod-openvino" "$MODULE_PATH" + +done + +exit 0 diff --git a/scripts/prepare-build.sh b/scripts/prepare-build.sh new file mode 100755 index 0000000..c771317 --- /dev/null +++ b/scripts/prepare-build.sh @@ -0,0 +1,58 @@ +#!/bin/bash + +set -e +set -x + +ROOT_DIR=$(pwd) +SOURCE_PATH=$(pwd)/sources +PACKAGE_PATH=$(pwd)/packages +BUILD_PATH=$(pwd)/build + +function download_release { + cd $SOURCE_PATH + local repo=$1 + local release=${2:+tags/$2} + release=${release:-latest} + local target_dir=$(basename $repo) + echo Downloading ${release} release of $repo into $target_dir + mkdir -p $target_dir + curl -sL https://api.github.com/repos/${repo}/releases/${release} + wget -O ${target_dir}/archive.tar.gz $(wget -q -O - https://api.github.com/repos/${repo}/releases/${release} | jq -r '.tarball_url') + tar --strip-components=1 -xzf ${target_dir}/archive.tar.gz -C $target_dir + rm -f ${target_dir}/archive.tar.gz + cd $ROOT_DIR +} + +function download_tarball { + cd "$SOURCE_PATH" || exit 1 + local repo=$1 + local url=$2 + local target_dir=$(basename "$repo") + echo "Downloading from $url into $target_dir" + mkdir -p "$target_dir" + + wget -O "${target_dir}/archive.tar.gz" "$url" + tar --strip-components=1 -xzf "${target_dir}/archive.tar.gz" -C "$target_dir" + rm -f "${target_dir}/archive.tar.gz" + cd "$ROOT_DIR" || exit 1 +} + +echo "Cleaning build directory..." +rm -rf $PACKAGE_PATH +rm -rf $BUILD_PATH +rm -rf $SOURCE_PATH + +mkdir -p $PACKAGE_PATH +mkdir -p $BUILD_PATH +mkdir -p $SOURCE_PATH + +# download dependencies +# download_release "audacity/audacity" +# download_release "intel/openvino-plugins-ai-audacity" +# download_release "ggerganov/whisper.cpp" "v1.6.0" +# download_release "openvinotoolkit/openvino_tokenizers" "2024.0.0.0" + +download_tarball "audacity/audacity" "https://github.com/audacity/audacity/archive/refs/tags/Audacity-3.7.3.tar.gz" +download_tarball "intel/openvino-plugins-ai-audacity" "https://github.com/intel/openvino-plugins-ai-audacity/archive/refs/tags/v3.7.1-R4.2.tar.gz" +download_tarball "ggerganov/whisper.cpp" "https://github.com/ggml-org/whisper.cpp/archive/refs/tags/v1.6.0.tar.gz" +download_tarball "openvinotoolkit/openvino_tokenizers" "https://github.com/openvinotoolkit/openvino_tokenizers/archive/refs/tags/2024.0.0.0.tar.gz" diff --git a/tools/ResourceDownloader/ResourceDownloader.xcodeproj/project.pbxproj b/tools/ResourceDownloader/ResourceDownloader.xcodeproj/project.pbxproj new file mode 100644 index 0000000..4306c96 --- /dev/null +++ b/tools/ResourceDownloader/ResourceDownloader.xcodeproj/project.pbxproj @@ -0,0 +1,538 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXContainerItemProxy section */ + 0A2E72612DBFC63900F87746 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0A2E724A2DBFC63800F87746 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 0A2E72512DBFC63800F87746; + remoteInfo = ResourceDownloader; + }; + 0A2E726B2DBFC63900F87746 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 0A2E724A2DBFC63800F87746 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 0A2E72512DBFC63800F87746; + remoteInfo = ResourceDownloader; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXFileReference section */ + 0A2E72522DBFC63800F87746 /* ResourceDownloader.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ResourceDownloader.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 0A2E72602DBFC63900F87746 /* ResourceDownloaderTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ResourceDownloaderTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 0A2E726A2DBFC63900F87746 /* ResourceDownloaderUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ResourceDownloaderUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 0A2E72542DBFC63800F87746 /* ResourceDownloader */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = ResourceDownloader; sourceTree = ""; }; + 0A2E72632DBFC63900F87746 /* ResourceDownloaderTests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = ResourceDownloaderTests; sourceTree = ""; }; + 0A2E726D2DBFC63900F87746 /* ResourceDownloaderUITests */ = {isa = PBXFileSystemSynchronizedRootGroup; explicitFileTypes = {}; explicitFolders = (); path = ResourceDownloaderUITests; sourceTree = ""; }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 0A2E724F2DBFC63800F87746 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 0A2E725D2DBFC63900F87746 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 0A2E72672DBFC63900F87746 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 0A2E72492DBFC63800F87746 = { + isa = PBXGroup; + children = ( + 0A2E72542DBFC63800F87746 /* ResourceDownloader */, + 0A2E72632DBFC63900F87746 /* ResourceDownloaderTests */, + 0A2E726D2DBFC63900F87746 /* ResourceDownloaderUITests */, + 0A2E72532DBFC63800F87746 /* Products */, + ); + sourceTree = ""; + }; + 0A2E72532DBFC63800F87746 /* Products */ = { + isa = PBXGroup; + children = ( + 0A2E72522DBFC63800F87746 /* ResourceDownloader.app */, + 0A2E72602DBFC63900F87746 /* ResourceDownloaderTests.xctest */, + 0A2E726A2DBFC63900F87746 /* ResourceDownloaderUITests.xctest */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 0A2E72512DBFC63800F87746 /* ResourceDownloader */ = { + isa = PBXNativeTarget; + buildConfigurationList = 0A2E72742DBFC63900F87746 /* Build configuration list for PBXNativeTarget "ResourceDownloader" */; + buildPhases = ( + 0A2E724E2DBFC63800F87746 /* Sources */, + 0A2E724F2DBFC63800F87746 /* Frameworks */, + 0A2E72502DBFC63800F87746 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 0A2E72542DBFC63800F87746 /* ResourceDownloader */, + ); + name = ResourceDownloader; + packageProductDependencies = ( + ); + productName = ResourceDownloader; + productReference = 0A2E72522DBFC63800F87746 /* ResourceDownloader.app */; + productType = "com.apple.product-type.application"; + }; + 0A2E725F2DBFC63900F87746 /* ResourceDownloaderTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 0A2E72772DBFC63900F87746 /* Build configuration list for PBXNativeTarget "ResourceDownloaderTests" */; + buildPhases = ( + 0A2E725C2DBFC63900F87746 /* Sources */, + 0A2E725D2DBFC63900F87746 /* Frameworks */, + 0A2E725E2DBFC63900F87746 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 0A2E72622DBFC63900F87746 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 0A2E72632DBFC63900F87746 /* ResourceDownloaderTests */, + ); + name = ResourceDownloaderTests; + packageProductDependencies = ( + ); + productName = ResourceDownloaderTests; + productReference = 0A2E72602DBFC63900F87746 /* ResourceDownloaderTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 0A2E72692DBFC63900F87746 /* ResourceDownloaderUITests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 0A2E727A2DBFC63900F87746 /* Build configuration list for PBXNativeTarget "ResourceDownloaderUITests" */; + buildPhases = ( + 0A2E72662DBFC63900F87746 /* Sources */, + 0A2E72672DBFC63900F87746 /* Frameworks */, + 0A2E72682DBFC63900F87746 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 0A2E726C2DBFC63900F87746 /* PBXTargetDependency */, + ); + fileSystemSynchronizedGroups = ( + 0A2E726D2DBFC63900F87746 /* ResourceDownloaderUITests */, + ); + name = ResourceDownloaderUITests; + packageProductDependencies = ( + ); + productName = ResourceDownloaderUITests; + productReference = 0A2E726A2DBFC63900F87746 /* ResourceDownloaderUITests.xctest */; + productType = "com.apple.product-type.bundle.ui-testing"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 0A2E724A2DBFC63800F87746 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1630; + LastUpgradeCheck = 1630; + TargetAttributes = { + 0A2E72512DBFC63800F87746 = { + CreatedOnToolsVersion = 16.3; + }; + 0A2E725F2DBFC63900F87746 = { + CreatedOnToolsVersion = 16.3; + TestTargetID = 0A2E72512DBFC63800F87746; + }; + 0A2E72692DBFC63900F87746 = { + CreatedOnToolsVersion = 16.3; + TestTargetID = 0A2E72512DBFC63800F87746; + }; + }; + }; + buildConfigurationList = 0A2E724D2DBFC63800F87746 /* Build configuration list for PBXProject "ResourceDownloader" */; + compatibilityVersion = "Xcode 12.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 0A2E72492DBFC63800F87746; + minimizedProjectReferenceProxies = 1; + productRefGroup = 0A2E72532DBFC63800F87746 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 0A2E72512DBFC63800F87746 /* ResourceDownloader */, + 0A2E725F2DBFC63900F87746 /* ResourceDownloaderTests */, + 0A2E72692DBFC63900F87746 /* ResourceDownloaderUITests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 0A2E72502DBFC63800F87746 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 0A2E725E2DBFC63900F87746 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 0A2E72682DBFC63900F87746 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 0A2E724E2DBFC63800F87746 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 0A2E725C2DBFC63900F87746 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 0A2E72662DBFC63900F87746 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 0A2E72622DBFC63900F87746 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 0A2E72512DBFC63800F87746 /* ResourceDownloader */; + targetProxy = 0A2E72612DBFC63900F87746 /* PBXContainerItemProxy */; + }; + 0A2E726C2DBFC63900F87746 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 0A2E72512DBFC63800F87746 /* ResourceDownloader */; + targetProxy = 0A2E726B2DBFC63900F87746 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 0A2E72722DBFC63900F87746 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 15.2; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 0A2E72732DBFC63900F87746 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MACOSX_DEPLOYMENT_TARGET = 15.2; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + }; + name = Release; + }; + 0A2E72752DBFC63900F87746 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = ResourceDownloader/ResourceDownloader.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 12.4; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.audacity.ResourceDownloader; + PRODUCT_NAME = "$(TARGET_NAME)"; + REGISTER_APP_GROUPS = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 0A2E72762DBFC63900F87746 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_ENTITLEMENTS = ResourceDownloader/ResourceDownloader.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + CURRENT_PROJECT_VERSION = 1; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_NSHumanReadableCopyright = ""; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + MACOSX_DEPLOYMENT_TARGET = 12.4; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.audacity.ResourceDownloader; + PRODUCT_NAME = "$(TARGET_NAME)"; + REGISTER_APP_GROUPS = YES; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 0A2E72782DBFC63900F87746 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 15.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.audacity.ResourceDownloaderTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ResourceDownloader.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ResourceDownloader"; + }; + name = Debug; + }; + 0A2E72792DBFC63900F87746 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + "CODE_SIGN_IDENTITY[sdk=macosx*]" = "-"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 15.2; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.audacity.ResourceDownloaderTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ResourceDownloader.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/ResourceDownloader"; + }; + name = Release; + }; + 0A2E727B2DBFC63900F87746 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.audacity.ResourceDownloaderUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = ResourceDownloader; + }; + name = Debug; + }; + 0A2E727C2DBFC63900F87746 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = org.audacity.ResourceDownloaderUITests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_TARGET_NAME = ResourceDownloader; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 0A2E724D2DBFC63800F87746 /* Build configuration list for PBXProject "ResourceDownloader" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0A2E72722DBFC63900F87746 /* Debug */, + 0A2E72732DBFC63900F87746 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 0A2E72742DBFC63900F87746 /* Build configuration list for PBXNativeTarget "ResourceDownloader" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0A2E72752DBFC63900F87746 /* Debug */, + 0A2E72762DBFC63900F87746 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 0A2E72772DBFC63900F87746 /* Build configuration list for PBXNativeTarget "ResourceDownloaderTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0A2E72782DBFC63900F87746 /* Debug */, + 0A2E72792DBFC63900F87746 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 0A2E727A2DBFC63900F87746 /* Build configuration list for PBXNativeTarget "ResourceDownloaderUITests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 0A2E727B2DBFC63900F87746 /* Debug */, + 0A2E727C2DBFC63900F87746 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 0A2E724A2DBFC63800F87746 /* Project object */; +} diff --git a/tools/ResourceDownloader/ResourceDownloader.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/tools/ResourceDownloader/ResourceDownloader.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/tools/ResourceDownloader/ResourceDownloader.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/tools/ResourceDownloader/ResourceDownloader/Assets.xcassets/AccentColor.colorset/Contents.json b/tools/ResourceDownloader/ResourceDownloader/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/tools/ResourceDownloader/ResourceDownloader/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/tools/ResourceDownloader/ResourceDownloader/Assets.xcassets/AppIcon.appiconset/Contents.json b/tools/ResourceDownloader/ResourceDownloader/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..3f00db4 --- /dev/null +++ b/tools/ResourceDownloader/ResourceDownloader/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,58 @@ +{ + "images" : [ + { + "idiom" : "mac", + "scale" : "1x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "16x16" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "32x32" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "128x128" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "256x256" + }, + { + "idiom" : "mac", + "scale" : "1x", + "size" : "512x512" + }, + { + "idiom" : "mac", + "scale" : "2x", + "size" : "512x512" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/tools/ResourceDownloader/ResourceDownloader/Assets.xcassets/Contents.json b/tools/ResourceDownloader/ResourceDownloader/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/tools/ResourceDownloader/ResourceDownloader/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/tools/ResourceDownloader/ResourceDownloader/ResourceDownloader.entitlements b/tools/ResourceDownloader/ResourceDownloader/ResourceDownloader.entitlements new file mode 100644 index 0000000..e00d841 --- /dev/null +++ b/tools/ResourceDownloader/ResourceDownloader/ResourceDownloader.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + com.apple.security.network.client + + + diff --git a/tools/ResourceDownloader/ResourceDownloader/ResourceDownloaderApp.swift b/tools/ResourceDownloader/ResourceDownloader/ResourceDownloaderApp.swift new file mode 100644 index 0000000..b9d1de2 --- /dev/null +++ b/tools/ResourceDownloader/ResourceDownloader/ResourceDownloaderApp.swift @@ -0,0 +1,591 @@ +import SwiftUI +import Combine + +struct Resource: Identifiable { + var id: UUID = UUID() + var name: String + var urls: [String] = [] +} + +class ModelStore: ObservableObject { + @Published var nodes: [Node] = [] + + var selectedItems: [Node] { + nodes.flatMap { $0.selectedItems } + } +} + +@main +struct TreeTableApp: App { + @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate + @StateObject private var modelStore = ModelStore() + @State private var currentView = CurrentScreen.welcome + var body: some Scene { + WindowGroup { + switch currentView { + case .welcome: + WelcomeView(onContinue: { + modelStore.nodes = loadNodesFromJSON(named: "models") + currentView = .selection + }) + case .selection: + ContentView( + modelStore: modelStore, + onBack: { + currentView = .welcome + }, + onContinue: { + currentView = .download + } + ) + .frame(width: 500, height: 400) + .onAppear() { + if let window = NSApplication.shared.windows.first { + window.center() + } + } + case .download: + DownloadView(currentView: $currentView, items: modelStore.selectedItems) + .frame(width: 500, height: 400) + case .conclusion: + CompletionView() + .frame(width: 500, height: 400) + } + } + } +} + +class AppDelegate: NSObject, NSApplicationDelegate, NSWindowDelegate { + func applicationDidFinishLaunching(_ notification: Notification) { + if let window = NSApplication.shared.windows.first { + window.delegate = self + window.setContentSize(NSSize(width: 500, height: 400)) + window.center() + } + } + + func windowWillClose(_ notification: Notification) { + NSApp.terminate(nil) + } +} + +enum CurrentScreen { + case welcome + case selection + case download + case conclusion +} + +struct WelcomeView: View { + var onContinue: () -> Void + + var body: some View { + VStack(spacing: 20) { + Image(systemName: "sparkles") + .resizable() + .scaledToFit() + .frame(width: 80, height: 80) + .foregroundColor(.accentColor) + + Text("Welcome to the Setup Wizard") + .font(.title) + .fontWeight(.bold) + + Text("This wizard will help you configure and launch OpenVINO in Audacity.") + .multilineTextAlignment(.center) + .padding() + + Button("Continue") { + onContinue() + } + .buttonStyle(.borderedProminent) + } + .padding() + .frame(width: 400, height: 300) + } +} + +enum CheckState { + case checked + case unchecked + case mixed +} + +struct FileInfo: Codable { + let url: String + let sha256: String + let dsize: Int + let esize: Int +} + +class Node: Identifiable, ObservableObject, Decodable { + let id = UUID() + let name: String + let description: String + let dir: String + let files: [FileInfo] + + @Published var state: CheckState = .unchecked + @Published var children: [Node]? + weak var parent: Node? + + var downloadSizeRecursive: Int { + let ownSize = files.map { $0.dsize }.reduce(0, +) + let childSize = (children ?? []) + .filter { $0.state == .checked } + .map { $0.downloadSize } + .reduce(0, +) + return ownSize + childSize + } + + var extractedSizeRecursive: Int { + let ownSize = files.map { $0.esize }.reduce(0, +) + let childSize = (children ?? []) + .filter { $0.state == .checked } + .map { $0.extractedSize } + .reduce(0, +) + return ownSize + childSize + } + + var downloadSize: Int { + return files.map { $0.dsize }.reduce(0, +) + } + + var extractedSize: Int { + return files.map { $0.esize }.reduce(0, +) + } + + var selectedItems: [Node] { + var results: [Node] = [] + + if state != .unchecked { + results.append(self) + } + + for child in children ?? [] { + results.append(contentsOf: child.selectedItems) + } + + return results + } + + enum CodingKeys: CodingKey { + case name, description, dir, files, children + } + + required init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.name = try container.decode(String.self, forKey: .name) + self.description = try container.decode(String.self, forKey: .description) + self.dir = try container.decode(String.self, forKey: .dir) + self.files = try container.decodeIfPresent([FileInfo].self, forKey: .files) ?? [] + self.children = try container.decodeIfPresent([Node].self, forKey: .children) + + self.children?.forEach { $0.parent = self } + } +} + +struct ContentView: View { + @ObservedObject var modelStore: ModelStore + @StateObject var viewModel = TreeViewModel() + + var onBack: () -> Void + var onContinue: () -> Void + + init(modelStore: ModelStore, onBack: @escaping () -> Void, onContinue: @escaping () -> Void) { + self.modelStore = modelStore + self.onBack = onBack + self.onContinue = onContinue + _viewModel = StateObject(wrappedValue: TreeViewModel(nodes: modelStore.nodes)) + } + + var body: some View { + VStack(spacing: 0) { + List { + OutlineGroup(viewModel.nodes, id: \.id, children: \.children) { node in + CheckboxView(node: node) + .onTapGesture { viewModel.toggle(node: node) } + } + } + .listStyle(.bordered(alternatesRowBackgrounds: false)) + + Divider() + + HStack { + SizeInfoView( + label: "Total Download Size:", + size: viewModel.totalDownloadSize + ) + SizeInfoView( + label: "Total Extracted Size:", + size: viewModel.totalExtractedSize + ) + } + .padding() + + HStack { + Spacer() + Button("Download selected", systemImage: "arrow.down.to.line") { + onContinue() + } + .buttonStyle(.borderedProminent) + Spacer() + } + .padding() + } + .navigationTitle("Download Wizard") + } +} + +struct CheckboxView: View { + @ObservedObject var node: Node + + var body: some View { + HStack(spacing: 8) { + VStack(alignment: .leading, spacing: 4) { + HStack { + Image(systemName: systemImageName) + .foregroundColor(node.state == .checked ? .blue : .primary) + Text(node.name) + .font(.body) + } + HStack(spacing: 16) { + Text("Download: \(node.downloadSizeRecursive.formatted(.byteCount(style: .file)))") + .foregroundColor(.secondary) + Text("Extracted: \(node.extractedSizeRecursive.formatted(.byteCount(style: .file)))") + .foregroundColor(.secondary) + } + .font(.caption) + } + } + .padding(.leading, node.children != nil ? 0 : 16) + } + + private var systemImageName: String { + switch node.state { + case .checked: return "checkmark.square.fill" + case .unchecked: return "square" + case .mixed: return "minus.square.fill" + } + } +} + +struct SizeInfoView: View { + let label: String + let size: Int + + var body: some View { + VStack(alignment: .leading) { + Text(label) + .font(.caption) + .foregroundColor(.secondary) + Text(size.formatted(.byteCount(style: .file))) + .font(.system(.body, design: .monospaced)) + } + .frame(maxWidth: .infinity, alignment: .leading) + .padding(8) + .background(RoundedRectangle(cornerRadius: 8).fill(Color(nsColor: .controlBackgroundColor))) + } +} + +class TreeViewModel: ObservableObject { + + @Published var nodes: [Node] + + @Published var totalDownloadSize = 0 + @Published var totalExtractedSize = 0 + + init(nodes: [Node] = []) { + self.nodes = nodes + self.totalDownloadSize = totalDownloadSize + self.totalExtractedSize = totalExtractedSize + } + + func toggle(node: Node) { + let newState: CheckState = node.state == .checked ? .unchecked : .checked + node.state = newState + updateChildren(node: node, state: newState) + updateParentState(node.parent) + calculateTotals() + } + + private func calculateTotals() { + totalDownloadSize = calculateTotal(for: \.downloadSizeRecursive) + totalExtractedSize = calculateTotal(for: \.extractedSizeRecursive) + } + + private func updateChildren(node: Node, state: CheckState) { + node.children?.forEach { + $0.state = state + updateChildren(node: $0, state: state) + } + } + + private func updateParentState(_ parent: Node?) { + guard let parent = parent else { return } + + let childrenStates = parent.children?.map { $0.state } ?? [] + let allChecked = childrenStates.allSatisfy { $0 == .checked } + let allUnchecked = childrenStates.allSatisfy { $0 == .unchecked } + + parent.state = allChecked ? .checked : + allUnchecked ? .unchecked : .mixed + + updateParentState(parent.parent) + } + + private func calculateTotal(for keyPath: KeyPath) -> Int { + var total = 0 + for node in nodes { + if node.state != .unchecked { + total += node[keyPath: keyPath] + } + } + return total + } +} + +private func loadNodesFromJSON(named filename: String) -> [Node] { + guard let url = Bundle.main.url(forResource: filename, withExtension: "json") else { + fatalError("Unable to find \(filename).json in bundle.") + } + + do { + let data = try Data(contentsOf: url) + let decoder = JSONDecoder() + return try decoder.decode([Node].self, from: data) + } catch { + print("Tried to load from: \(url)") + print("Error: \(error)") + fatalError("Failed to load or decode \(filename).json: \(error)") + } +} + +struct DownloadItem: Identifiable { + let id = UUID() + let item: Node + var progress: Double = 0.0 + var isDownloading = false + var isCompleted = false +} + +class DownloadManager: ObservableObject { + @Published var items: [DownloadItem] + @Published var currentDownloadItem: DownloadItem? + @Published var completedItems = 0 { + didSet { + if completedItems == items.count { + isFinished = true + } + } + } + @Published var isFinished: Bool + + private var cancellables = Set() + + init(items: [Node]) { + self.items = items.map { DownloadItem(item: $0) } + self.isFinished = false + } + + func startDownloads() { + processNextDownload() + } + + private func processNextDownload() { + guard let nextItem = items.first(where: { !$0.isCompleted && !$0.isDownloading }) else { + return + } + + guard !nextItem.item.files.isEmpty else { + if let index = items.firstIndex(where: { $0.id == nextItem.id }) { + items[index].isCompleted = true + } + processNextDownload() + return + } + + var currentItem = nextItem + currentItem.isDownloading = true + if let index = items.firstIndex(where: { $0.id == currentItem.id }) { + items[index] = currentItem + } + currentDownloadItem = currentItem + + for file in currentItem.item.files { + let url = URL(string: file.url)! + let request = URLRequest(url: url) + let session = URLSession.shared + let task = session.downloadTask(with: request) { [weak self] tempURL, response, error in + guard let self = self, let tempURL = tempURL else { return } + + + let homeDirectory = FileManager.default.homeDirectoryForCurrentUser + var destinationDir = homeDirectory.appendingPathComponent("Library/Application Support/audacity/openvino-models") + + destinationDir = destinationDir.appendingPathComponent(currentItem.item.dir, isDirectory: true) + do { + // Create the directory if it doesn't exist + try FileManager.default.createDirectory(at: destinationDir, withIntermediateDirectories: true) + print("Directory created or already exists at: \(destinationDir.path)") + } catch { + print("Failed to create directory: \(error)") + } + + do { + try FileManager.default.createDirectory(at: destinationDir, withIntermediateDirectories: true) + + let filename = url.lastPathComponent + let destinationPath = destinationDir.appendingPathComponent(filename) + + // Move downloaded file to destination + try FileManager.default.moveItem(at: tempURL, to: destinationPath) + + // Extract based on file extension + if filename.hasSuffix(".zip") { + let unzipTask = Process() + unzipTask.executableURL = URL(fileURLWithPath: "/usr/bin/unzip") + unzipTask.arguments = ["-o", destinationPath.path, "-d", destinationDir.path] + try unzipTask.run() + unzipTask.waitUntilExit() + try FileManager.default.removeItem(at: destinationPath) + } else if filename.hasSuffix(".tar.gz") { + let tarTask = Process() + tarTask.executableURL = URL(fileURLWithPath: "/usr/bin/tar") + tarTask.arguments = ["-xzf", destinationPath.path, "-C", destinationDir.path] + try tarTask.run() + tarTask.waitUntilExit() + try FileManager.default.removeItem(at: destinationPath) + } + + } catch { + print("Error during file handling: \(error)") + } + + DispatchQueue.main.async { + if let index = self.items.firstIndex(where: { $0.id == currentItem.id }) { + self.items[index].isDownloading = false + self.items[index].isCompleted = true + self.completedItems += 1 + self.currentDownloadItem = nil + self.processNextDownload() + } + } + } + + let progressObserver = task.progress.observe(\.fractionCompleted) { [weak self] progress, _ in + DispatchQueue.main.async { + if let index = self?.items.firstIndex(where: { $0.id == currentItem.id }) { + self?.items[index].progress = progress.fractionCompleted + self?.currentDownloadItem = self?.items[index] + } + } + } + + cancellables.insert(AnyCancellable { + progressObserver.invalidate() + }) + + task.resume() + } + } +} + +struct DownloadView: View { + @Binding var currentView: CurrentScreen + @StateObject private var downloadManager: DownloadManager + private let totalItems: Int + var buttonText: String { + downloadManager.completedItems == totalItems ? "Finish" : "Cancel" + } + + init(currentView: Binding, items: [Node]) { + _currentView = currentView + _downloadManager = StateObject(wrappedValue: DownloadManager(items: items)) + totalItems = items.count + } + + var body: some View { + VStack(spacing: 20) { + // Overall progress + VStack { + Text("Total Progress") + .font(.headline) + ProgressView(value: Double(downloadManager.completedItems), total: Double(totalItems)) + Text("\(downloadManager.completedItems)/\(totalItems) items downloaded") + .font(.caption) + } + .padding() + + // Current item progress + if let currentItem = downloadManager.currentDownloadItem { + VStack { + let itemName = currentItem.item + let label = itemName.parent != nil + ? "Downloading: \(itemName.parent!.name): \(itemName.name)" + : "Downloading: \(itemName.name)" + + Text(label) + .font(.headline) + ProgressView(value: currentItem.progress) + Text("\(Int(currentItem.progress * 100))% complete") + .font(.caption) + } + .padding() + .transition(.opacity) + } + } + .padding() + .onAppear { + downloadManager.startDownloads() + } + .onChange(of: downloadManager.isFinished) { finished in + if (finished) { + currentView = .conclusion + } + } + + .navigationTitle("Download Wizard") + } +} + +struct CompletionView: View { + var body: some View { + VStack(spacing: 20) { + Image(systemName: "checkmark.seal.fill") + .resizable() + .scaledToFit() + .frame(width: 80, height: 80) + .foregroundColor(.green) + + Text("All Done!") + .font(.title) + .fontWeight(.bold) + + Text("All components were installed successfully. To start using OpenVINO features in Audacity, please restart the application.") + .font(.body) + .multilineTextAlignment(.center) + .padding(.horizontal) + + Text("Once restarted, you'll be able to access enhanced AI-powered effects and tools powered by OpenVINO.") + .font(.footnote) + .multilineTextAlignment(.center) + .foregroundColor(.secondary) + .padding(.horizontal) + Spacer() + HStack { + Spacer() + Button("Finish") { + closeWindow() + } + .buttonStyle(.borderedProminent) + Spacer() + } + } + .padding() + .navigationTitle("Download Wizard") + } + func closeWindow() { + NSApp.keyWindow?.close() + } +} diff --git a/tools/ResourceDownloader/ResourceDownloader/models.json b/tools/ResourceDownloader/ResourceDownloader/models.json new file mode 100644 index 0000000..66403b1 --- /dev/null +++ b/tools/ResourceDownloader/ResourceDownloader/models.json @@ -0,0 +1,256 @@ +[ + { + "name": "Audio Super Resolution", + "description": "Versatile Audio Super Resolution", + "dir": "audiosr", + "files": [ + { + "url": "https://huggingface.co/Intel/versatile_audio_super_resolution_openvino/resolve/9a97d7f128b22aea72e92862a3eccc310f88ac26/versatile_audio_sr_base_openvino_models.zip?download=true", + "sha256": "e4058862616e8eaa157a6a69d75daff49b63f997c69e59887c77a04ce6840d86", + "dsize": 930565310, + "esize": 234 + } + ], + "children": [ + { + "name": "Basic", + "description": "Basic", + "dir": "audiosr", + "files": [ + { + "url": "https://huggingface.co/Intel/versatile_audio_super_resolution_openvino/resolve/9a97d7f128b22aea72e92862a3eccc310f88ac26/versatile_audio_sr_ddpm_basic_openvino_models.zip?download=true", + "sha256": "382e0693e35e4bbeae1a0f3223954250e13427108ae16c0fe8f786489bbe2767", + "dsize": 473639145, + "esize": 234 + } + ] + }, + { + "name": "Speech", + "description": "Speech", + "dir": "audiosr", + "files": [ + { + "url": "https://huggingface.co/Intel/versatile_audio_super_resolution_openvino/resolve/9a97d7f128b22aea72e92862a3eccc310f88ac26/versatile_audio_sr_ddpm_speech_openvino_models.zip?download=true", + "sha256": "f44ee433da9e1aa6dd948142d81523c7a58213acfa0699a4014e1f6d30e8a349", + "dsize": 473616589, + "esize": 234 + } + ] + } + ] + }, + { + "name": "MusicGen", + "description": "MusicGen", + "dir": "musicgen", + "files": [ + { + "url": "https://huggingface.co/Intel/musicgen-static-openvino/resolve/b2ad8083f3924ed704814b68c5df9cbbf2ad2aae/musicgen_small_enc_dec_tok_openvino_models.zip?download=true", + "sha256": "f5cdd695a52cf4c0619dd419c8bff4e71700ee805b547e86b7483423883d6f32", + "dsize": 279450559, + "esize": 234 + } + ], + "children":[ + { + "name": "Small (mono)", + "description": "Mono", + "dir": "musicgen", + "files": [ + { + "url": "https://huggingface.co/Intel/musicgen-static-openvino/resolve/b2ad8083f3924ed704814b68c5df9cbbf2ad2aae/musicgen_small_stereo_openvino_models.zip?download=true", + "sha256": "1c6496267b22175ecdd2518b9ed8c9870454674fc08fac1c864ca62e3d87e8e7", + "dsize": 1154648066, + "esize": 234 + } + ] + }, + { + "name": "Small (stereo)", + "description": "Stereo", + "dir": "musicgen", + "files": [ + { + "url": "https://huggingface.co/Intel/musicgen-static-openvino/resolve/b2ad8083f3924ed704814b68c5df9cbbf2ad2aae/musicgen_small_mono_openvino_models.zip?download=true", + "sha256": "b1e47df3ec60d0c5f70ba664fea636df9bc94710269ee72357a3353fd579afd9", + "dsize": 1098243900, + "esize": 234 + } + ] + } + ] + }, + { + "name": "Whisper", + "description": "Whisper Base", + "dir": ".", + "files": [ + { + "url": "https://huggingface.co/Intel/whisper.cpp-openvino-models/resolve/194efafbee09390bbf33f33d02d9856dacfdd162/ggml-base-models.zip?download=true", + "sha256": "18f6e87146bfe9986fa764657cca26695bcb7fa3ba5d77d6d6af1d38de720e4c", + "dsize": 171306938, + "esize": 234 + } + ], + "children":[ + { + "name": "Small", + "description": "Small", + "dir": ".", + "files": [ + { + "url": "https://huggingface.co/Intel/whisper.cpp-openvino-models/resolve/194efafbee09390bbf33f33d02d9856dacfdd162/ggml-small-models.zip?download=true", + "sha256": "7efabdcb08ec09ee24b615db9fe86425114a46d87a25d1e3d14ecd7195dd4e9f", + "dsize": 610456149, + "esize": 234 + } + ] + }, + { + "name": "Small (English only)", + "description": "Small English", + "dir": ".", + "files": [ + { + "url": "https://huggingface.co/Intel/whisper.cpp-openvino-models/resolve/194efafbee09390bbf33f33d02d9856dacfdd162/ggml-small.en-tdrz-models.zip?download=true", + "sha256": "ea825cb105e48aa0796e332c93827339dd60aa1a61633df14bb6611bcef26b95", + "dsize": 609030010, + "esize": 234 + } + ] + }, + { + "name": "Medium", + "description": "Medium", + "dir": ".", + "files": [ + { + "url": "https://huggingface.co/Intel/whisper.cpp-openvino-models/resolve/5c2f20da06aa17198dbd778d4190383557c23b1b/ggml-medium-models.zip?download=true", + "sha256": "afd961c0bac5830dbcbd15826f49553a5d5fb8a09c7a24eb634304fa068fe59f", + "dsize": 2110515739, + "esize": 234 + } + ] + }, + { + "name": "Large v1", + "description": "Large V1", + "dir": ".", + "files": [ + { + "url": "https://huggingface.co/Intel/whisper.cpp-openvino-models/resolve/9731937f2b9ca0aff8b8acd031ed77a1a378cc72/ggml-large-v1-models.zip?download=true", + "sha256": "fa187861eb46b701f242d63fc0067878de6345a71714d926c4b0eecf1ec0fffa", + "dsize": 4301056953, + "esize": 234 + } + ] + }, + { + "name": "Large v2", + "description": "Large V2", + "dir": ".", + "files": [ + { + "url": "https://huggingface.co/Intel/whisper.cpp-openvino-models/resolve/dc08ae819b895594cae08b0210bee051462aba75/ggml-large-v2-models.zip?download=true", + "sha256": "ab306b0a0a93a731e56efbfa4e80448206bd9b5b863d5a99e40a3532d64ec754", + "dsize": 4285702502, + "esize": 234 + } + ] + }, + { + "name": "Large v3", + "description": "Large V3", + "dir": ".", + "files": [ + { + "url": "https://huggingface.co/Intel/whisper.cpp-openvino-models/resolve/d0c3075c40e4938fb2a4cccfc91704106d3e7d12/ggml-large-v3-models.zip?download=true", + "sha256": "d61160dc5b1c1abc1dbdd6ae571fe4da87079fa3170156408f8775b493c15cdd", + "dsize": 4277163902, + "esize": 234 + } + ] + } + ] + }, + { + "name": "Noise Suppression", + "description": "Noise Suppression", + "dir": ".", + "files": [], + "children":[ + { + "name": "Deep Filter Net 2", + "description": "Deep Filter Net 2", + "dir": ".", + "files": [ + { + "url": "https://huggingface.co/Intel/deepfilternet-openvino/resolve/995706bda3da69da0825074ba7dbc8a78067e980/deepfilternet2.zip?download=true", + "sha256": "9d10c9c77a64121587e7d94a3d4d5cfeb67e9bfd2e416d48d1f74ac725c4f66e", + "dsize": 8648697, + "esize": 234 + } + ] + }, + { + "name": "Deep Filter Net 3", + "description": "Deep Filter Net 3", + "dir": ".", + "files": [ + { + "url": "https://huggingface.co/Intel/deepfilternet-openvino/resolve/995706bda3da69da0825074ba7dbc8a78067e980/deepfilternet3.zip?download=true", + "sha256": "fdc74e11439ca09a106f5bd77ed24002ef4d0e02114238a271c069d06172f341", + "dsize": 8004431, + "esize": 234 + } + ] + }, + { + "name": "Dense UNet", + "description": "Dense UNet", + "dir": ".", + "files": [ + { + "url": "https://storage.openvinotoolkit.org/repositories/open_model_zoo/2023.0/models_bin/1/noise-suppression-denseunet-ll-0001/FP16/noise-suppression-denseunet-ll-0001.bin", + "sha256": "da59b41a656b2948a4b45580b4870614d2dfa071a7566555aea4547423888e08", + "dsize": 8625568, + "esize": 234 + }, + { + "url": "https://storage.openvinotoolkit.org/repositories/open_model_zoo/2023.0/models_bin/1/noise-suppression-denseunet-ll-0001/FP16/noise-suppression-denseunet-ll-0001.xml", + "sha256": "89116a01cc59f7ac3f1f1365c851e47c9089fd33ca86712c30dc1f0ee7d14803", + "dsize": 689820, + "esize": 234 + } + ] + } + ] + }, + { + "name": "Music Separation", + "description": "Music Separation", + "dir": ".", + "children":[ + { + "name": "Basic", + "description": "Basic", + "dir": ".", + "files": [ + { + "url": "https://huggingface.co/Intel/demucs-openvino/resolve/97fc578fb57650045d40b00bc84c7d156be77547/htdemucs_v4.bin?download=true", + "sha256": "7aa84fa1f2b534bd6865a5609b8b5b028802fe761d6a09b1d30a1564f8fac6f8", + "dsize": 101167138, + "esize": 234 + }, + { + "url": "https://huggingface.co/Intel/demucs-openvino/resolve/97fc578fb57650045d40b00bc84c7d156be77547/htdemucs_v4.xml?download=true", + "sha256": "304e24325756089d6bb6583171dd1bea2327505e87c6cb6e010afea7463d9f0a", + "dsize": 1875040, + "esize": 234 + } + ] + } + ] + } +] diff --git a/tools/ResourceDownloader/ResourceDownloaderTests/ResourceDownloaderTests.swift b/tools/ResourceDownloader/ResourceDownloaderTests/ResourceDownloaderTests.swift new file mode 100644 index 0000000..df7d5c0 --- /dev/null +++ b/tools/ResourceDownloader/ResourceDownloaderTests/ResourceDownloaderTests.swift @@ -0,0 +1,10 @@ +import Testing +@testable import ResourceDownloader + +struct ResourceDownloaderTests { + + @Test func example() async throws { + // Write your test here and use APIs like `#expect(...)` to check expected conditions. + } + +} diff --git a/tools/ResourceDownloader/ResourceDownloaderUITests/ResourceDownloaderUITests.swift b/tools/ResourceDownloader/ResourceDownloaderUITests/ResourceDownloaderUITests.swift new file mode 100644 index 0000000..e1cea04 --- /dev/null +++ b/tools/ResourceDownloader/ResourceDownloaderUITests/ResourceDownloaderUITests.swift @@ -0,0 +1,34 @@ +import XCTest + +final class ResourceDownloaderUITests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + + // In UI tests it is usually best to stop immediately when a failure occurs. + continueAfterFailure = false + + // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + @MainActor + func testExample() throws { + // UI tests must launch the application that they test. + let app = XCUIApplication() + app.launch() + + // Use XCTAssert and related functions to verify your tests produce the correct results. + } + + @MainActor + func testLaunchPerformance() throws { + // This measures how long it takes to launch your application. + measure(metrics: [XCTApplicationLaunchMetric()]) { + XCUIApplication().launch() + } + } +} diff --git a/tools/ResourceDownloader/ResourceDownloaderUITests/ResourceDownloaderUITestsLaunchTests.swift b/tools/ResourceDownloader/ResourceDownloaderUITests/ResourceDownloaderUITestsLaunchTests.swift new file mode 100644 index 0000000..139b7ba --- /dev/null +++ b/tools/ResourceDownloader/ResourceDownloaderUITests/ResourceDownloaderUITestsLaunchTests.swift @@ -0,0 +1,26 @@ +import XCTest + +final class ResourceDownloaderUITestsLaunchTests: XCTestCase { + + override class var runsForEachTargetApplicationUIConfiguration: Bool { + true + } + + override func setUpWithError() throws { + continueAfterFailure = false + } + + @MainActor + func testLaunch() throws { + let app = XCUIApplication() + app.launch() + + // Insert steps here to perform after app launch but before taking a screenshot, + // such as logging into a test account or navigating somewhere in the app + + let attachment = XCTAttachment(screenshot: app.screenshot()) + attachment.name = "Launch Screen" + attachment.lifetime = .keepAlways + add(attachment) + } +}