diff --git a/.augment-guidelines b/.augment-guidelines index 5629e3ea..1e4dcf3a 100644 --- a/.augment-guidelines +++ b/.augment-guidelines @@ -254,21 +254,110 @@ All source files (doesn't inlcude FFmpeg code) should include Apache 2.0 license ### Building the Project +OpenConverter supports two build modes: + +#### **Debug Mode (Development)** +Fast builds for development, uses system libraries: + ```bash # Create build directory mkdir build && cd build -# Configure with CMake -cmake ../src -DENABLE_GUI=ON +# Configure (Debug is default) +cmake ../src -DENABLE_GUI=ON -DBMF_TRANSCODER=ON # Build make -j4 # Run -./OpenConverter # GUI mode -./OpenConverter --help # CLI mode +./OpenConverter.app/Contents/MacOS/OpenConverter # macOS +./OpenConverter # Linux +``` + +**What's bundled:** +- ✅ Python modules (`enhance_module.py`) +- ✅ AI model weights (2.4 MB) +- ❌ BMF libraries (uses system BMF from CMake path) +- ❌ Qt frameworks (uses system Qt) +- ❌ Python runtime (uses system Python) + +**Requirements:** +- Python 3.9+ with PyTorch, Real-ESRGAN, OpenCV, NumPy +- BMF framework (path configured in CMake) +- Qt 5.15+ (for GUI) + +**Environment:** No environment variables needed! BMF path is detected from CMake configuration. + +#### **Release Mode (Distribution)** +Standalone builds for distribution, bundles everything: + +```bash +# Create separate build directory +mkdir build-release && cd build-release + +# Configure with Release mode +cmake ../src -DCMAKE_BUILD_TYPE=Release -DENABLE_GUI=ON -DBMF_TRANSCODER=ON + +# Build (takes longer due to bundling) +make -j4 + +# Run - NO environment variables needed! +open OpenConverter.app # macOS +./OpenConverter # Linux ``` +**What's bundled:** +- ✅ Python modules +- ✅ AI model weights (2.4 MB) +- ✅ BMF libraries (25+ libraries, ~30 MB) +- ✅ Qt frameworks (10+ frameworks, ~150 MB) +- ✅ Python runtime with PyTorch, Real-ESRGAN, OpenCV, NumPy (~30 MB) +- ✅ FFmpeg libraries (~25 MB) + +**Total Size:** ~800 MB - 1.2 GB (fully standalone) + +**Advantages:** +- ✅ Fully standalone (no dependencies) +- ✅ Works on any Mac (macOS 11+) +- ✅ No environment variables needed +- ✅ Ready for distribution (zip/DMG) + +### Library Bundling (macOS Release Mode) + +**Important:** All library bundling (Qt, FFmpeg, BMF) is handled by **`tool/fix_macos_libs.sh`**, NOT by CMake. + +#### **Workflow** + +1. **Build**: `cmake -B build-release -DCMAKE_BUILD_TYPE=Release && cd build-release && make -j4` +2. **Bundle**: `cd .. && tool/fix_macos_libs.sh` + +The script auto-detects build directory, bundles Qt/FFmpeg/BMF libraries, fixes paths, and code signs. + +#### **BMF Library Structure** + +``` +OpenConverter.app/Contents/ +├── Frameworks/ +│ ├── lib/ # Builtin modules (BMF hardcoded path) +│ │ ├── libbuiltin_modules.dylib +│ │ ├── libcopy_module.dylib +│ │ └── libcvtcolor.dylib +│ ├── libbmf_module_sdk.dylib # Core BMF libraries +│ ├── libhmp.dylib +│ ├── _bmf.cpython-39-darwin.so +│ └── BUILTIN_CONFIG.json +└── Resources/bmf_python/ # BMF Python package +``` + +**Why `Frameworks/lib/`?** BMF expects builtin modules in `lib/` subdirectory relative to `BMF_MODULE_CONFIG_PATH`. + +**Script Logic:** +- Auto-appends `/output/bmf` to `$BMF_ROOT_PATH` if needed (same as CMake) +- Copies builtin modules to `Frameworks/lib/` +- Copies core libraries to `Frameworks/` +- Processes both `.dylib` and `.so` files +- Fixes all paths to use `@executable_path` + ### Translation Workflow OpenConverter uses Qt Linguist tools for internationalization (i18n). Translation files are located in `src/resources/`. diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml new file mode 100644 index 00000000..54aef7a1 --- /dev/null +++ b/.github/workflows/build.yaml @@ -0,0 +1,516 @@ +name: Build + +on: + pull_request: + types: [opened, synchronize, reopened] + push: + tags: + - "v*.*.*" + release: + types: + - created + workflow_dispatch: + +jobs: + build-linux: + strategy: + matrix: + include: + - arch: x86_64 + runner: ubuntu-22.04 + ffmpeg_url: https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2024-11-30-13-12/ffmpeg-n5.1.6-11-gcde3c5fc0c-linux64-gpl-shared-5.1.tar.xz + ffmpeg_dir: ffmpeg-n5.1.6-11-gcde3c5fc0c-linux64-gpl-shared-5.1 + bmf_url: https://github.com/OpenConverterLab/bmf/releases/download/oc0.0.3/bmf-bin-linux-x86_64-cp39.tar.gz + appimagetool: appimagetool-x86_64.AppImage + - arch: aarch64 + runner: ubuntu-22.04-arm + ffmpeg_url: https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2024-11-30-13-12/ffmpeg-n5.1.6-11-gcde3c5fc0c-linuxarm64-gpl-shared-5.1.tar.xz + ffmpeg_dir: ffmpeg-n5.1.6-11-gcde3c5fc0c-linuxarm64-gpl-shared-5.1 + bmf_url: https://github.com/OpenConverterLab/bmf/releases/download/oc0.0.3/bmf-bin-linux-aarch64-cp39.tar.gz + appimagetool: appimagetool-aarch64.AppImage + runs-on: ${{ matrix.runner }} + concurrency: + group: "review-linux-${{ matrix.arch }}-${{ github.event.pull_request.number }}" + cancel-in-progress: true + + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Print current branch and commit hash + run: | + echo "Current branch: $(git rev-parse --abbrev-ref HEAD)" + echo "Current commit hash: $(git rev-parse HEAD)" + echo "Architecture: ${{ matrix.arch }}" + + - name: Install dependencies + run: | + sudo apt update + sudo apt install -y make git pkg-config cmake gcc g++ wget libgl1 + + - name: Get FFmpeg + run: | + wget ${{ matrix.ffmpeg_url }} + tar xJvf ${{ matrix.ffmpeg_dir }}.tar.xz + ls ${{ matrix.ffmpeg_dir }} + echo "FFMPEG_ROOT_PATH=$(pwd)/${{ matrix.ffmpeg_dir }}" >> $GITHUB_ENV + + - name: Get BMF + run: | + wget ${{ matrix.bmf_url }} + tar xzvf bmf-bin-linux-${{ matrix.arch }}-cp39.tar.gz + echo "BMF_ROOT_PATH=$(pwd)/output/bmf" >> $GITHUB_ENV + + - name: Set up Qt + run: | + sudo apt-get install -y qt5-qmake qtbase5-dev qtchooser qtbase5-dev-tools cmake build-essential + + - name: Build with CMake + run: | + export PATH=$PATH:$FFMPEG_ROOT_PATH/bin + (cd src && cmake -B build && cd build && make -j$(nproc)) + + - name: Copy libs + run: | + export LD_LIBRARY_PATH=$FFMPEG_ROOT_PATH/lib/:$BMF_ROOT_PATH/lib + export LIBRARY_PATH=$FFMPEG_ROOT_PATH/lib/:$BMF_ROOT_PATH/lib + # linuxdeployqt + sudo apt-get -y install git g++ libgl1-mesa-dev + git clone https://github.com/probonopd/linuxdeployqt.git + # Then build in Qt Creator, or use + export PATH=$(readlink -f /tmp/.mount_QtCreator-*-${{ matrix.arch }}/*/gcc_64/bin/):$PATH + (cd linuxdeployqt && qmake && make && sudo make install) + # patchelf + wget https://nixos.org/releases/patchelf/patchelf-0.9/patchelf-0.9.tar.bz2 + tar xf patchelf-0.9.tar.bz2 + ( cd patchelf-0.9/ && ./configure && make && sudo make install ) + # appimage + sudo wget -c "https://github.com/AppImage/AppImageKit/releases/download/continuous/${{ matrix.appimagetool }}" -O /usr/local/bin/appimagetool + sudo chmod a+x /usr/local/bin/appimagetool + (linuxdeployqt/bin/linuxdeployqt ./src/build/OpenConverter -appimage) + # clean up + rm -rf CMake* Makefile cmake_install.cmake OpenConverter_autogen/ doc/ + continue-on-error: true + + + - name: Copy runtime + run: | + cp $FFMPEG_ROOT_PATH/lib/libswscale.so.6 src/build/lib + cp $FFMPEG_ROOT_PATH/lib/libavfilter.so.8 src/build/lib + cp $FFMPEG_ROOT_PATH/lib/libpostproc.so.56 src/build/lib + cp $FFMPEG_ROOT_PATH/lib/libavdevice.so.59 src/build/lib + cp $BMF_ROOT_PATH/lib/libbuiltin_modules.so src/build/lib + cp $BMF_ROOT_PATH/lib/libbmf_py_loader.so src/build/lib + cp $BMF_ROOT_PATH/BUILTIN_CONFIG.json src/build + touch src/build/run.sh + echo export LD_LIBRARY_PATH="~/.local/share/OpenConverter/Python.framework/lib:./lib" >> src/build/run.sh + echo ./OpenConverter >> src/build/run.sh + cp src/resources/requirements.txt src/build/requirements.txt + cp -r $BMF_ROOT_PATH src/build/ + (mkdir -p src/build/modules/weights && + cd src/build/modules/weights && + wget https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.5.0/realesr-animevideov3.pth) + + # Step to package the build directory + - name: Create tar.gz package + run: | + BUILD_DIR="src/build" + PACKAGE_NAME="OpenConverter_Linux_${{ matrix.arch }}.tar.gz" + OUTPUT_DIR="OpenConverter_Linux_${{ matrix.arch }}" + mkdir -p $OUTPUT_DIR + cp -r $BUILD_DIR/* $OUTPUT_DIR/ + tar -czvf $PACKAGE_NAME $OUTPUT_DIR + rm -rf $OUTPUT_DIR + + # Step to upload the tar.gz package as an artifact + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: OpenConverter_Linux_${{ matrix.arch }} + path: OpenConverter_Linux_${{ matrix.arch }}.tar.gz + + # - name: Setup tmate session + # if: ${{ failure() }} + # uses: mxschmitt/action-tmate@v3 + + - name: Finish + run: echo "Build complete" + + build-linglong: + needs: build-linux + strategy: + matrix: + include: + - arch: x86_64 + runner: ubuntu-24.04 + - arch: aarch64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + concurrency: + group: "review-linglong-${{ matrix.arch }}-${{ github.event.pull_request.number }}" + cancel-in-progress: true + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Remove unnecessary directories to free up space + run: | + sudo rm -rf /usr/local/.ghcup + sudo rm -rf /opt/hostedtoolcache/CodeQL + sudo rm -rf /usr/local/lib/android/sdk/ndk + sudo rm -rf /usr/share/dotnet + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/share/boost + + - name: Download Linux build artifact + uses: actions/download-artifact@v4 + with: + name: OpenConverter_Linux_${{ matrix.arch }} + + - name: Download linglong-builder cache + run: | + docker run --rm -v ~/.cache/:/target ghcr.io/jacklau1222/ll-cache-${{ matrix.arch }}:latest \ + bash -c "cp -r /root/.cache/linglong-builder /target/" + + sudo chown -R "$USER:$USER" ~/.cache/linglong-builder + sudo chmod -R 755 ~/.cache/linglong-builder + du -sh ~/.cache/linglong-builder + + - name: Install Linglong tools + run: | + echo "deb [trusted=yes] https://ci.deepin.com/repo/obs/linglong:/CI:/release/xUbuntu_24.04/ ./" | sudo tee /etc/apt/sources.list.d/linglong.list + sudo apt update + sudo apt install -y linglong-bin linglong-builder linglong-box + + - name: build the desktop file + run: | + cd src/resources + touch default.desktop + echo "[Desktop Entry]" >> default.desktop + echo "Type=Application" >> default.desktop + echo "Name=OpenConverter" >> default.desktop + echo "Exec=/opt/apps/io.github.openconverterlab/files/bin/run.sh" >> default.desktop + echo "Icon=default" >> default.desktop + echo "Categories=Media;Video;Audio;Converter;" >> default.desktop + echo "Comment=OpenConverter Application" >> default.desktop + echo "Terminal=false" >> default.desktop + cat default.desktop + + - name: Prepare Linglong build directory + run: | + # Extract the artifact + tar -xzvf OpenConverter_Linux_${{ matrix.arch }}.tar.gz + + # Create ll-builder directory structure + mkdir -p ll-builder/binary + mkdir -p ll-builder/template_app/applications + mkdir -p ll-builder/template_app/icons/hicolor/500x500/apps + + # Copy binary files from artifact + cp -r OpenConverter_Linux_${{ matrix.arch }}/* ll-builder/binary/ + + # Copy linglong.yaml + cp src/resources/linglong.yaml ll-builder/ + + # Copy desktop file + cp src/resources/default.desktop ll-builder/template_app/applications/ + + # Copy icon file + cp src/resources/OpenConverter-logo.png ll-builder/template_app/icons/hicolor/500x500/apps/ + + - name: Build Linglong package + run: | + cd ll-builder + ll-builder build + ll-builder export --layer --no-develop + + - name: Setup tmate session + if: ${{ failure() }} + uses: mxschmitt/action-tmate@v3 + + - name: Upload Linglong package + uses: actions/upload-artifact@v4 + with: + name: OpenConverter_Linglong_${{ matrix.arch }} + path: ll-builder/*.layer + + - name: Finish + run: echo "Linglong build complete" + + build-macos-arm: + runs-on: macos-14 + concurrency: + group: "review-macos-${{ github.event.pull_request.number }}" + cancel-in-progress: true + + steps: + - name: Checkout target branch code (using pull_request) + uses: actions/checkout@v2 + + - name: Print current branch and commit hash + run: | + echo "Current branch: $(git rev-parse --abbrev-ref HEAD)" + echo "Current commit hash: $(git rev-parse HEAD)" + + - name: Install FFmpeg and Qt via Homebrew + run: | + # Install FFmpeg 5 with x264, x265 support (pre-built from Homebrew) + brew install ffmpeg@5 qt@5 python@3.9 + + # Set FFmpeg path + export FFMPEG_ROOT_PATH=$(brew --prefix ffmpeg@5) + echo "FFMPEG_ROOT_PATH=$FFMPEG_ROOT_PATH" >> $GITHUB_ENV + + # Verify FFmpeg has x264 and x265 + echo "FFmpeg configuration:" + $FFMPEG_ROOT_PATH/bin/ffmpeg -version | head -n 1 + $FFMPEG_ROOT_PATH/bin/ffmpeg -encoders 2>/dev/null | grep -E "libx264|libx265" || echo "Warning: x264/x265 not found" + + - name: Checkout BMF repository(specific branch) + run: | + git clone https://github.com/OpenConverterLab/bmf.git + + # wget https://invisible-island.net/archives/ncurses/ncurses-6.5.tar.gz + # wget https://ftp.gnu.org/gnu/binutils/binutils-2.43.1.tar.bz2 + + # - name: Cache ncurses build + # uses: actions/cache@v3 + # with: + # path: opt/ncurses + # key: ${{ runner.os }}-ncurses-${{ hashFiles('ncurses-6.5.tar.gz') }} + # restore-keys: | + # ${{ runner.os }}-ncurses- + + # - name: Cache binutils build + # uses: actions/cache@v3 + # with: + # path: opt/binutils + # key: ${{ runner.os }}-binutils-${{ hashFiles('binutils-2.43.1.tar.bz2') }} + # restore-keys: | + # ${{ runner.os }}-binutils- + + # - name: compile dependencies + # run: | + # if [ ! -d "$(pwd)/opt/ncurses" ]; then + # tar -xzvf ncurses-6.5.tar.gz + # (cd ncurses-6.5 && ./configure --prefix=/Users/runner/work/OpenConverter/OpenConverter/opt/ncurses && make -j$(sysctl -n hw.ncpu) && sudo make install) + # else + # echo "ncurses is already installed, skipping build." + # fi + + # if [ ! -d "$(pwd)/opt/binutils" ]; then + # tar xvf binutils-2.43.1.tar.bz2 + # (cd binutils-2.43.1 && ./configure --prefix=/Users/runner/work/OpenConverter/OpenConverter/opt/binutils --enable-install-libiberty && make -j$(sysctl -n hw.ncpu) && sudo make install) + # else + # echo "binutils is already installed, skipping build." + # fi + + # - name: Cache BMF build + # uses: actions/cache@v3 + # with: + # path: bmf/output/ + # key: ${{ runner.os }}-bmf-${{ hashFiles('bmf/build.sh') }} + # restore-keys: | + # ${{ runner.os }}-bmf-macos-arm- + + - name: Set up BMF if not cached + run: | + if [ ! -d "$(pwd)/bmf/output/" ]; then + # export LIBRARY_PATH=$(pwd)/opt/binutils/lib:$LIBRARY_PATH + # export CMAKE_PREFIX_PATH=$(pwd)/opt/binutils:$CMAKE_PREFIX_PATH + brew link --force python@3.9 + export BMF_PYTHON_VERSION="3.9" + pip install setuptools + (cd bmf && git checkout oc && git submodule update --init --recursive && ./build_osx.sh) + else + echo "BMF is already installed, skipping build." + fi + echo "BMF_ROOT_PATH=$(pwd)/bmf/output/bmf" >> $GITHUB_ENV + + - name: Build and Deploy + run: | + export PATH="$(brew --prefix ffmpeg@5)/bin:$PATH" + export CMAKE_PREFIX_PATH="$(brew --prefix qt@5):$CMAKE_PREFIX_PATH" + export QT_DIR="$(brew --prefix qt@5)/lib/cmake/Qt5" + export PATH="$(brew --prefix qt@5)/bin:$PATH" + + cd src + cmake -B build -DCMAKE_BUILD_TYPE=Release \ + -DFFMPEG_ROOT_PATH="$(brew --prefix ffmpeg@5)" \ + -DBMF_TRANSCODER=ON + + cd build + make -j$(sysctl -n hw.ncpu) + + # Use the fix_macos_libs.sh script to handle deployment + cd .. + chmod +x ../tool/fix_macos_libs.sh + ../tool/fix_macos_libs.sh + + cd build + + # Create DMG using simple shell script + echo "Creating DMG..." + chmod +x ../../tool/create_dmg_simple.sh + ../../tool/create_dmg_simple.sh OpenConverter.app + + cd ../.. + mv src/build/OpenConverter.dmg OpenConverter_macOS_aarch64.dmg + + # Step to upload the dmg package as an artifact + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: OpenConverter_macOS_aarch64 + path: OpenConverter_macOS_aarch64.dmg + + # - name: Setup tmate session + # if: ${{ failure() }} + # uses: mxschmitt/action-tmate@v3 + + - name: Finish + run: echo "Release upload complete" + + build-windows-x64: + runs-on: windows-latest + concurrency: + group: "review-win-${{ github.event.pull_request.number }}" + cancel-in-progress: true + + steps: + # Check out the repository code. + - name: Checkout repository + uses: actions/checkout@v2 + + # Set up the Qt environment. + - name: (2) Install Qt + uses: jurplel/install-qt-action@v3 + with: + version: 6.4.3 + host: windows + target: desktop + arch: win64_msvc2019_64 + dir: ${{ runner.temp }} + setup-python: false + + # Download FFmpeg from the specified release URL. + - name: Download FFmpeg + shell: powershell + run: | + $ffmpegUrl = "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2024-11-30-13-12/ffmpeg-n5.1.6-11-gcde3c5fc0c-win64-gpl-shared-5.1.zip" + $outputZip = "ffmpeg.zip" + Invoke-WebRequest -Uri $ffmpegUrl -OutFile $outputZip + Expand-Archive -Path $outputZip -DestinationPath ffmpeg + echo "FFMPEG_ROOT_PATH=$(pwd)/ffmpeg/ffmpeg-n5.1.6-11-gcde3c5fc0c-win64-gpl-shared-5.1" >> $GITHUB_ENV + + # Create a build directory, run qmake, and build the project. + - name: Build Qt project + run: | + (cd src && + cmake -S . -B build "-DFFMPEG_ROOT_PATH=../ffmpeg/ffmpeg-n5.1.6-11-gcde3c5fc0c-win64-gpl-shared-5.1" -DFFTOOL_TRANSCODER=OFF -DBMF_TRANSCODER=OFF && + cmake --build build --config Release --parallel) + + - name : Deploy project + run: | + # 1) Create the deploy folder under the repo workspace + New-Item -ItemType Directory -Force -Path OpenConverter_win64 + + # 2) Copy your built exe into OpenConverter_win64/ + Copy-Item -Path "src\build\Release\OpenConverter.exe" -Destination "OpenConverter_win64" + + # 3) Bundle Qt runtime into OpenConverter_win64/ + & "D:\a\_temp\Qt\6.4.3\msvc2019_64\bin\windeployqt.exe" ` + "--qmldir=src" ` + "OpenConverter_win64\OpenConverter.exe" + + # 4) Copy FFmpeg DLLs into OpenConverter_win64/ + Copy-Item ` + -Path "ffmpeg\ffmpeg-n5.1.6-11-gcde3c5fc0c-win64-gpl-shared-5.1\bin\*.dll" ` + -Destination "OpenConverter_win64" + + # Upload the build artifacts (upload-artifact will automatically zip the folder) + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: OpenConverter_win64 + path: OpenConverter_win64 + + # - name: Setup tmate session + # if: ${{ failure() }} + # uses: mxschmitt/action-tmate@v3 + + - name: Finish + run: echo "Windows x64 build complete" + + # Upload all artifacts to GitHub Release (only runs on tag push or release creation) + upload-release: + if: startsWith(github.ref, 'refs/tags/') + needs: [build-linux, build-linglong, build-macos-arm, build-windows-x64] + runs-on: ubuntu-latest + + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: List downloaded artifacts + run: | + echo "Downloaded artifacts:" + ls -la artifacts/ + find artifacts -type f + + - name: Prepare release packages + run: | + cd artifacts + + # Linux x86_64 - already a tar.gz + if [ -f "OpenConverter_Linux_x86_64/OpenConverter_Linux_x86_64.tar.gz" ]; then + cp OpenConverter_Linux_x86_64/OpenConverter_Linux_x86_64.tar.gz ../OpenConverter_Linux_x86_64.tar.gz + fi + + # Linux aarch64 - already a tar.gz + if [ -f "OpenConverter_Linux_aarch64/OpenConverter_Linux_aarch64.tar.gz" ]; then + cp OpenConverter_Linux_aarch64/OpenConverter_Linux_aarch64.tar.gz ../OpenConverter_Linux_aarch64.tar.gz + fi + + # Linglong x86_64 - layer file + if [ -d "OpenConverter_Linglong_x86_64" ]; then + cp OpenConverter_Linglong_x86_64/*.layer ../OpenConverter_Linglong_x86_64.layer || true + fi + + # Linglong aarch64 - layer file + if [ -d "OpenConverter_Linglong_aarch64" ]; then + cp OpenConverter_Linglong_aarch64/*.layer ../OpenConverter_Linglong_aarch64.layer || true + fi + + # macOS aarch64 - already a dmg + if [ -f "OpenConverter_macOS_aarch64/OpenConverter_macOS_aarch64.dmg" ]; then + cp OpenConverter_macOS_aarch64/OpenConverter_macOS_aarch64.dmg ../OpenConverter_macOS_aarch64.dmg + fi + + # Windows x64 - create zip from folder + if [ -d "OpenConverter_win64" ]; then + cd OpenConverter_win64 + zip -r ../../OpenConverter_win64.zip . + cd .. + fi + + cd .. + echo "Release packages:" + ls -la *.tar.gz *.dmg *.zip *.layer 2>/dev/null || echo "Some packages may be missing" + + - name: Upload Release Assets + uses: softprops/action-gh-release@v1 + with: + files: | + OpenConverter_Linux_x86_64.tar.gz + OpenConverter_Linux_aarch64.tar.gz + OpenConverter_Linglong_x86_64.layer + OpenConverter_Linglong_aarch64.layer + OpenConverter_macOS_aarch64.dmg + OpenConverter_win64.zip + + - name: Finish + run: echo "Release upload complete" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1a35dd76..064ed009 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,7 +37,7 @@ jobs: - name: Build with CMake run: | export PATH=$PATH:$FFMPEG_ROOT_PATH/bin - (cd src && cmake -B build -DENABLE_TESTS=ON -DENABLE_GUI=OFF && cd build && make -j$(nproc)) + (cd src && cmake -B build -DENABLE_TESTS=ON -DBMF_TRANSCODER=OFF -DENABLE_GUI=OFF && cd build && make -j$(nproc)) - name: Run tests run: | @@ -78,6 +78,35 @@ jobs: $FFMPEG_ROOT_PATH/bin/ffmpeg -version | head -n 1 $FFMPEG_ROOT_PATH/bin/ffmpeg -encoders 2>/dev/null | grep -E "libx264|libx265" || echo "Warning: x264/x265 not found" + - name: Checkout BMF repository(specific branch) + run: | + git clone https://github.com/OpenConverterLab/bmf.git + cd bmf + git checkout oc + + # wget https://invisible-island.net/archives/ncurses/ncurses-6.5.tar.gz + # wget https://ftp.gnu.org/gnu/binutils/binutils-2.43.1.tar.bz2 + + - name: Cache BMF build + uses: actions/cache@v3 + with: + path: bmf/output/ + key: ${{ runner.os }}-bmf-${{ hashFiles('bmf/build_osx.sh') }} + restore-keys: | + ${{ runner.os }}-bmf-macos-arm- + + - name: Set up BMF if not cached + run: | + if [ ! -d "$(pwd)/bmf/output/" ]; then + # export LIBRARY_PATH=$(pwd)/opt/binutils/lib:$LIBRARY_PATH + # export CMAKE_PREFIX_PATH=$(pwd)/opt/binutils:$CMAKE_PREFIX_PATH + pip install setuptools + (cd bmf && git checkout oc && git submodule update --init --recursive && ./build_osx.sh) + else + echo "BMF is already installed, skipping build." + fi + echo "BMF_ROOT_PATH=$(pwd)/bmf/output/bmf" >> $GITHUB_ENV + - name: Build with CMake run: | export PATH=$PATH:$FFMPEG_ROOT_PATH/bin diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml deleted file mode 100644 index d1d5661e..00000000 --- a/.github/workflows/release.yaml +++ /dev/null @@ -1,257 +0,0 @@ -name: Release - -on: - release: - types: - - created - push: - tags: - - "v*.*.*" - -jobs: - build-linux-x86: - runs-on: ubuntu-22.04 - - steps: - - name: Checkout code - uses: actions/checkout@v3 - - - name: Checkout BMF repository (specific branch) - run: | - sudo apt install -y nasm yasm libx264-dev libx265-dev libnuma-dev - git clone https://github.com/JackLau1222/bmf.git - - - name: Get FFmpeg - run: | - wget https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2024-11-30-13-12/ffmpeg-n5.1.6-11-gcde3c5fc0c-linux64-gpl-shared-5.1.tar.xz - tar xJvf ffmpeg-n5.1.6-11-gcde3c5fc0c-linux64-gpl-shared-5.1.tar.xz - ls ffmpeg-n5.1.6-11-gcde3c5fc0c-linux64-gpl-shared-5.1 - echo "FFMPEG_ROOT_PATH=$(pwd)/ffmpeg-n5.1.6-11-gcde3c5fc0c-linux64-gpl-shared-5.1" >> $GITHUB_ENV - - - name: Set up Qt - run: | - sudo apt-get install -y qt5-qmake qtbase5-dev qtchooser qtbase5-dev-tools cmake build-essential - - - name: Build with CMake - run: | - export PATH=$PATH:$FFMPEG_ROOT_PATH/bin - (cd src && cmake -B build -DBMF_TRANSCODER=OFF && cd build && make -j$(nproc)) - - - name: Copy libs - run: | - export LD_LIBRARY_PATH=$FFMPEG_ROOT_PATH/lib/:$BMF_ROOT_PATH/lib - export LIBRARY_PATH=$FFMPEG_ROOT_PATH/lib/:$BMF_ROOT_PATH/lib - # linuxdeployqt - sudo apt-get -y install git g++ libgl1-mesa-dev - git clone https://github.com/probonopd/linuxdeployqt.git - # Then build in Qt Creator, or use - export PATH=$(readlink -f /tmp/.mount_QtCreator-*-x86_64/*/gcc_64/bin/):$PATH - (cd linuxdeployqt && qmake && make && sudo make install) - # patchelf - wget https://nixos.org/releases/patchelf/patchelf-0.9/patchelf-0.9.tar.bz2 - tar xf patchelf-0.9.tar.bz2 - ( cd patchelf-0.9/ && ./configure && make && sudo make install ) - # appimage - sudo wget -c "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" -O /usr/local/bin/appimagetool - sudo chmod a+x /usr/local/bin/appimagetool - (linuxdeployqt/bin/linuxdeployqt ./src/build/OpenConverter -appimage) - continue-on-error: true - - # Step to package the build directory - - name: Create tar.gz package - run: | - BUILD_DIR="src/build" - PACKAGE_NAME="OpenConverter_Linux_x86.tar.gz" - OUTPUT_DIR="OpenConverter_Linux_x86" - mkdir -p $OUTPUT_DIR - cp -r $BUILD_DIR/* $OUTPUT_DIR/ - tar -czvf $PACKAGE_NAME -C $OUTPUT_DIR . - rm -rf $OUTPUT_DIR - - # Step to upload the tar.gz package as an artifact - - name: Upload build artifact - uses: actions/upload-artifact@v4 - with: - name: OpenConverter_Linux_x86 - path: OpenConverter_Linux_x86.tar.gz - - - - name: Get GitHub Release information - id: release_info - run: echo "::set-output name=release_tag::$(echo ${GITHUB_REF#refs/tags/})" - - - - name: Upload Release Asset - uses: softprops/action-gh-release@v1 - if: startsWith(github.ref, 'refs/tags/') - with: - files: OpenConverter_Linux_x86.tar.gz - - - name: Finish - run: echo "Linux X86 Release upload complete" - - build-mac-arm: - runs-on: macos-latest - - steps: - - name: Checkout target branch code (using pull_request_target) - uses: actions/checkout@v2 - - - name: Install FFmpeg and Qt via Homebrew - run: | - # Install FFmpeg 5 with x264, x265 support (pre-built from Homebrew) - brew install ffmpeg@5 qt@5 - - # Set FFmpeg path - export FFMPEG_ROOT_PATH=$(brew --prefix ffmpeg@5) - echo "FFMPEG_ROOT_PATH=$FFMPEG_ROOT_PATH" >> $GITHUB_ENV - - # Verify FFmpeg has x264 and x265 - echo "FFmpeg configuration:" - $FFMPEG_ROOT_PATH/bin/ffmpeg -version | head -n 1 - $FFMPEG_ROOT_PATH/bin/ffmpeg -encoders 2>/dev/null | grep -E "libx264|libx265" || echo "Warning: x264/x265 not found" - - - name: Build and Deploy - run: | - export PATH="$(brew --prefix ffmpeg@5)/bin:$PATH" - export CMAKE_PREFIX_PATH="$(brew --prefix qt@5):$CMAKE_PREFIX_PATH" - export QT_DIR="$(brew --prefix qt@5)/lib/cmake/Qt5" - export PATH="$(brew --prefix qt@5)/bin:$PATH" - - cd src - cmake -B build \ - -DFFMPEG_ROOT_PATH="$(brew --prefix ffmpeg@5)" \ - -DBMF_TRANSCODER=OFF - - cd build - make -j$(sysctl -n hw.ncpu) - - # Use the fix_macos_libs.sh script to handle deployment - cd .. - chmod +x ../tool/fix_macos_libs.sh - ../tool/fix_macos_libs.sh - - cd build - - # Create DMG using simple shell script - echo "Creating DMG..." - chmod +x ../../tool/create_dmg_simple.sh - ../../tool/create_dmg_simple.sh OpenConverter.app - - cd ../.. - mv src/build/OpenConverter.dmg OpenConverter_macOS_aarch64.dmg - - # Step to upload the dmg package as an artifact - - name: Upload build artifact - uses: actions/upload-artifact@v4 - with: - name: OpenConverter_macOS_aarch64 - path: OpenConverter_macOS_aarch64.dmg - - - name: Get GitHub Release information - id: release_info - run: echo "::set-output name=release_tag::$(echo ${GITHUB_REF#refs/tags/})" - - - name: Upload Release Asset - uses: softprops/action-gh-release@v1 - if: startsWith(github.ref, 'refs/tags/') - with: - files: OpenConverter_macOS_aarch64.dmg - - - name: Finish - run: echo "macOS aarch64 Release upload complete" - - # - name: Setup tmate session - # if: ${{ failure() }} - # uses: mxschmitt/action-tmate@v3 - - - name: Finish - run: echo "Release upload complete" - - build-windows-x64: - runs-on: windows-latest - - steps: - # Check out the repository code. - - name: Checkout repository - uses: actions/checkout@v2 - - # Set up the Qt environment. - - name: (2) Install Qt - uses: jurplel/install-qt-action@v3 - with: - version: 6.4.3 - host: windows - target: desktop - arch: win64_msvc2019_64 - dir: ${{ runner.temp }} - setup-python: false - - # Download FFmpeg from the specified release URL. - - name: Download FFmpeg - shell: powershell - run: | - $ffmpegUrl = "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2024-11-30-13-12/ffmpeg-n5.1.6-11-gcde3c5fc0c-win64-gpl-shared-5.1.zip" - $outputZip = "ffmpeg.zip" - Invoke-WebRequest -Uri $ffmpegUrl -OutFile $outputZip - Expand-Archive -Path $outputZip -DestinationPath ffmpeg - echo "FFMPEG_ROOT_PATH=$(pwd)/ffmpeg/ffmpeg-n5.1.6-11-gcde3c5fc0c-win64-gpl-shared-5.1" >> $GITHUB_ENV - - # Create a build directory, run qmake, and build the project. - - name: Build Qt project - run: | - (cd src && - cmake -S . -B build "-DFFMPEG_ROOT_PATH=../ffmpeg/ffmpeg-n5.1.6-11-gcde3c5fc0c-win64-gpl-shared-5.1" -DFFTOOL_TRANSCODER=OFF && - cmake --build build --config Release --parallel) - - - name : Deploy project - run: | - # 1) Create the deploy folder under the repo workspace - New-Item -ItemType Directory -Force -Path OpenConverter_win64 - - # 2) Copy your built exe into OpenConverter_win64/ - Copy-Item -Path "src\build\Release\OpenConverter.exe" -Destination "OpenConverter_win64" - - # 3) Bundle Qt runtime into OpenConverter_win64/ - & "D:\a\_temp\Qt\6.4.3\msvc2019_64\bin\windeployqt.exe" ` - "--qmldir=src" ` - "OpenConverter_win64\OpenConverter.exe" - - # 4) Copy FFmpeg DLLs into OpenConverter_win64/ - Copy-Item ` - -Path "ffmpeg\ffmpeg-n5.1.6-11-gcde3c5fc0c-win64-gpl-shared-5.1\bin\*.dll" ` - -Destination "OpenConverter_win64" - - # Upload the build artifacts (upload-artifact will automatically zip the folder) - - name: Upload build artifacts - uses: actions/upload-artifact@v4 - with: - name: OpenConverter_win64 - path: OpenConverter_win64 - - - name: Get GitHub Release information - id: release_info - run: echo "::set-output name=release_tag::$(echo ${GITHUB_REF#refs/tags/})" - - # Create zip for release (only compress once for release upload) - - name: Create release package - if: startsWith(github.ref, 'refs/tags/') - shell: pwsh - run: | - Compress-Archive -Path "OpenConverter_win64" -DestinationPath "OpenConverter_win64.zip" - - - name: Upload Release Asset - uses: softprops/action-gh-release@v1 - if: startsWith(github.ref, 'refs/tags/') - with: - files: OpenConverter_win64.zip - - - name: Finish - run: echo "win64 Release upload complete" - - # - name: Setup tmate session - # if: ${{ failure() }} - # uses: mxschmitt/action-tmate@v3 - - - name: Finish - run: echo "Release upload complete" diff --git a/.github/workflows/review.yaml b/.github/workflows/review.yaml deleted file mode 100644 index ce0f1629..00000000 --- a/.github/workflows/review.yaml +++ /dev/null @@ -1,261 +0,0 @@ -name: Review - -on: - pull_request: - types: [opened, synchronize, reopened] - workflow_dispatch: - -jobs: - build-linux-x86: - runs-on: ubuntu-22.04 - concurrency: - group: "review-linux-${{ github.event.pull_request.number }}" - cancel-in-progress: true - - steps: - - name: Checkout code - uses: actions/checkout@v2 - - - name: Print current branch and commit hash - run: | - echo "Current branch: $(git rev-parse --abbrev-ref HEAD)" - echo "Current commit hash: $(git rev-parse HEAD)" - - - name: Checkout BMF repository (specific branch) - run: | - # sudo apt update - # sudo apt install -y make git pkg-config libssl-dev cmake binutils-dev libgoogle-glog-dev gcc g++ golang wget libgl1 - sudo apt install -y nasm yasm libx264-dev libx265-dev libnuma-dev - # sudo apt install -y python3.9 python3-dev python3-pip libsndfile1 libsndfile1-dev - - git clone https://github.com/JackLau1222/bmf.git - - # - name: Cache BMF build - # uses: actions/cache@v3 - # with: - # path: bmf/output/ - # key: ${{ runner.os }}-bmf-${{ hashFiles('bmf/build.sh') }} - # restore-keys: | - # ${{ runner.os }}-bmf-linux-x86 - - - name: Get FFmpeg - run: | - wget https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2024-11-30-13-12/ffmpeg-n5.1.6-11-gcde3c5fc0c-linux64-gpl-shared-5.1.tar.xz - tar xJvf ffmpeg-n5.1.6-11-gcde3c5fc0c-linux64-gpl-shared-5.1.tar.xz - ls ffmpeg-n5.1.6-11-gcde3c5fc0c-linux64-gpl-shared-5.1 - echo "FFMPEG_ROOT_PATH=$(pwd)/ffmpeg-n5.1.6-11-gcde3c5fc0c-linux64-gpl-shared-5.1" >> $GITHUB_ENV - - # - name: Set up BMF if not cached - # run: | - # if [ ! -d "$(pwd)/bmf/output/" ]; then - # (cd bmf && git checkout fork_by_oc && ./build.sh) - # else - # echo "BMF is already installed, skipping build." - # fi - # echo "BMF_ROOT_PATH=$(pwd)/bmf/output/bmf" >> $GITHUB_ENV - - - name: Set up Qt - run: | - sudo apt-get install -y qt5-qmake qtbase5-dev qtchooser qtbase5-dev-tools cmake build-essential - - - name: Build with CMake - run: | - export PATH=$PATH:$FFMPEG_ROOT_PATH/bin - (cd src && cmake -B build -DBMF_TRANSCODER=OFF && cd build && make -j$(nproc)) - - - name: Copy libs - run: | - export LD_LIBRARY_PATH=$FFMPEG_ROOT_PATH/lib/:$BMF_ROOT_PATH/lib - export LIBRARY_PATH=$FFMPEG_ROOT_PATH/lib/:$BMF_ROOT_PATH/lib - # linuxdeployqt - sudo apt-get -y install git g++ libgl1-mesa-dev - git clone https://github.com/probonopd/linuxdeployqt.git - # Then build in Qt Creator, or use - export PATH=$(readlink -f /tmp/.mount_QtCreator-*-x86_64/*/gcc_64/bin/):$PATH - (cd linuxdeployqt && qmake && make && sudo make install) - # patchelf - wget https://nixos.org/releases/patchelf/patchelf-0.9/patchelf-0.9.tar.bz2 - tar xf patchelf-0.9.tar.bz2 - ( cd patchelf-0.9/ && ./configure && make && sudo make install ) - # appimage - sudo wget -c "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" -O /usr/local/bin/appimagetool - sudo chmod a+x /usr/local/bin/appimagetool - (linuxdeployqt/bin/linuxdeployqt ./src/build/OpenConverter -appimage) - continue-on-error: true - - - # - name: Copy runtime - # run: | - # cp $FFMPEG_ROOT_PATH/lib/libswscale.so.6 src/build/lib - # cp $FFMPEG_ROOT_PATH/lib/libavfilter.so.8 src/build/lib - # cp $FFMPEG_ROOT_PATH/lib/libpostproc.so.56 src/build/lib - # cp $BMF_ROOT_PATH/lib/libbuiltin_modules.so src/build/lib - # cp $BMF_ROOT_PATH/BUILTIN_CONFIG.json src/build - # touch src/build/activate_env.sh - # echo export LD_LIBRARY_PATH="./lib" >> src/build/activate_env.sh - - # Step to package the build directory - - name: Create tar.gz package - run: | - BUILD_DIR="src/build" - PACKAGE_NAME="OpenConverter_Linux_x86.tar.gz" - OUTPUT_DIR="OpenConverter_Linux_x86" - mkdir -p $OUTPUT_DIR - cp -r $BUILD_DIR/* $OUTPUT_DIR/ - tar -czvf $PACKAGE_NAME -C $OUTPUT_DIR . - rm -rf $OUTPUT_DIR - - # Step to upload the tar.gz package as an artifact - - name: Upload build artifact - uses: actions/upload-artifact@v4 - with: - name: OpenConverter_Linux_x86 - path: OpenConverter_Linux_x86.tar.gz - - # - name: Setup tmate session - # if: ${{ failure() }} - # uses: mxschmitt/action-tmate@v3 - - - name: Finish - run: echo "Release upload complete" - - build-macos-arm: - runs-on: macos-latest - concurrency: - group: "review-macos-${{ github.event.pull_request.number }}" - cancel-in-progress: true - - steps: - - name: Checkout target branch code (using pull_request) - uses: actions/checkout@v2 - - - name: Print current branch and commit hash - run: | - echo "Current branch: $(git rev-parse --abbrev-ref HEAD)" - echo "Current commit hash: $(git rev-parse HEAD)" - - - name: Install FFmpeg and Qt via Homebrew - run: | - # Install FFmpeg 5 with x264, x265 support (pre-built from Homebrew) - brew install ffmpeg@5 qt@5 - - # Set FFmpeg path - export FFMPEG_ROOT_PATH=$(brew --prefix ffmpeg@5) - echo "FFMPEG_ROOT_PATH=$FFMPEG_ROOT_PATH" >> $GITHUB_ENV - - # Verify FFmpeg has x264 and x265 - echo "FFmpeg configuration:" - $FFMPEG_ROOT_PATH/bin/ffmpeg -version | head -n 1 - $FFMPEG_ROOT_PATH/bin/ffmpeg -encoders 2>/dev/null | grep -E "libx264|libx265" || echo "Warning: x264/x265 not found" - - - name: Build and Deploy - run: | - export PATH="$(brew --prefix ffmpeg@5)/bin:$PATH" - export CMAKE_PREFIX_PATH="$(brew --prefix qt@5):$CMAKE_PREFIX_PATH" - export QT_DIR="$(brew --prefix qt@5)/lib/cmake/Qt5" - export PATH="$(brew --prefix qt@5)/bin:$PATH" - - cd src - cmake -B build \ - -DFFMPEG_ROOT_PATH="$(brew --prefix ffmpeg@5)" \ - -DBMF_TRANSCODER=OFF - - cd build - make -j$(sysctl -n hw.ncpu) - - # Use the fix_macos_libs.sh script to handle deployment - cd .. - chmod +x ../tool/fix_macos_libs.sh - ../tool/fix_macos_libs.sh - - cd build - - # Create DMG using simple shell script - echo "Creating DMG..." - chmod +x ../../tool/create_dmg_simple.sh - ../../tool/create_dmg_simple.sh OpenConverter.app - - cd ../.. - mv src/build/OpenConverter.dmg OpenConverter_macOS_aarch64.dmg - - # Step to upload the dmg package as an artifact - - name: Upload build artifact - uses: actions/upload-artifact@v4 - with: - name: OpenConverter_macOS_aarch64 - path: OpenConverter_macOS_aarch64.dmg - - # - name: Setup tmate session - # if: ${{ failure() }} - # uses: mxschmitt/action-tmate@v3 - - - name: Finish - run: echo "Release upload complete" - - build-windows-x64: - runs-on: windows-latest - concurrency: - group: "review-win-${{ github.event.pull_request.number }}" - cancel-in-progress: true - - steps: - # Check out the repository code. - - name: Checkout repository - uses: actions/checkout@v2 - - # Set up the Qt environment. - - name: (2) Install Qt - uses: jurplel/install-qt-action@v3 - with: - version: 6.4.3 - host: windows - target: desktop - arch: win64_msvc2019_64 - dir: ${{ runner.temp }} - setup-python: false - - # Download FFmpeg from the specified release URL. - - name: Download FFmpeg - shell: powershell - run: | - $ffmpegUrl = "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2024-11-30-13-12/ffmpeg-n5.1.6-11-gcde3c5fc0c-win64-gpl-shared-5.1.zip" - $outputZip = "ffmpeg.zip" - Invoke-WebRequest -Uri $ffmpegUrl -OutFile $outputZip - Expand-Archive -Path $outputZip -DestinationPath ffmpeg - echo "FFMPEG_ROOT_PATH=$(pwd)/ffmpeg/ffmpeg-n5.1.6-11-gcde3c5fc0c-win64-gpl-shared-5.1" >> $GITHUB_ENV - - # Create a build directory, run qmake, and build the project. - - name: Build Qt project - run: | - (cd src && - cmake -S . -B build "-DFFMPEG_ROOT_PATH=../ffmpeg/ffmpeg-n5.1.6-11-gcde3c5fc0c-win64-gpl-shared-5.1" -DFFTOOL_TRANSCODER=OFF && - cmake --build build --config Release --parallel) - - - name : Deploy project - run: | - # 1) Create the deploy folder under the repo workspace - New-Item -ItemType Directory -Force -Path OpenConverter_win64 - - # 2) Copy your built exe into OpenConverter_win64/ - Copy-Item -Path "src\build\Release\OpenConverter.exe" -Destination "OpenConverter_win64" - - # 3) Bundle Qt runtime into OpenConverter_win64/ - & "D:\a\_temp\Qt\6.4.3\msvc2019_64\bin\windeployqt.exe" ` - "--qmldir=src" ` - "OpenConverter_win64\OpenConverter.exe" - - # 4) Copy FFmpeg DLLs into OpenConverter_win64/ - Copy-Item ` - -Path "ffmpeg\ffmpeg-n5.1.6-11-gcde3c5fc0c-win64-gpl-shared-5.1\bin\*.dll" ` - -Destination "OpenConverter_win64" - - # Upload the build artifacts (upload-artifact will automatically zip the folder) - - name: Upload build artifacts - uses: actions/upload-artifact@v4 - with: - name: OpenConverter_win64 - path: OpenConverter_win64 - - # - name: Setup tmate session - # if: ${{ failure() }} - # uses: mxschmitt/action-tmate@v3 diff --git a/.gitignore b/.gitignore index 9ceae138..8eae1efd 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,8 @@ src/build-* build .idea workload_output.txt + +# AI model weights (downloaded during build) +src/modules/weights/*.pth +src/modules/weights/*.onnx +src/modules/weights/*.pt diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 69eb4461..9427f1ff 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -16,7 +16,7 @@ endif() option(ENABLE_GUI "enable GUI" ON) option(ENABLE_TESTS "enable unit tests" OFF) # BMF is experimental feature, so we can't enable it by default -option(BMF_TRANSCODER "enable BMF Transcoder" OFF) +option(BMF_TRANSCODER "enable BMF Transcoder" ON) option(FFTOOL_TRANSCODER "enable FFmpeg Command Tool Transcoder" ON) option(FFMPEG_TRANSCODER "enable FFmpeg Core Transcoder" ON) @@ -71,9 +71,18 @@ endif() if(BMF_TRANSCODER) if(DEFINED ENV{BMF_ROOT_PATH}) set(BMF_ROOT_PATH $ENV{BMF_ROOT_PATH}) + # If BMF_ROOT_PATH doesn't end with /output/bmf, append it + if(NOT BMF_ROOT_PATH MATCHES "output/bmf$") + set(BMF_ROOT_PATH "${BMF_ROOT_PATH}/output/bmf") + endif() else() set(BMF_ROOT_PATH "/Users/jacklau/Documents/Programs/Git/Github/bmf/output/bmf") endif() + message(STATUS "Using BMF from: ${BMF_ROOT_PATH}") + + # Pass BMF_ROOT_PATH to C++ code as compile definition + add_definitions(-DBMF_ROOT_PATH_STR="${BMF_ROOT_PATH}") + include_directories(${BMF_ROOT_PATH}/include) link_directories(${BMF_ROOT_PATH}/lib) add_definitions(-DENABLE_BMF) @@ -226,6 +235,16 @@ target_include_directories(OpenConverterCore PUBLIC ${FFMPEG_INCLUDE_DIRS} ) +# Copy Python modules to build directory (if BMF is enabled) +if(BMF_TRANSCODER) + add_custom_command( + TARGET OpenConverterCore POST_BUILD + COMMAND ${CMAKE_COMMAND} -E make_directory ${CMAKE_BINARY_DIR}/modules + COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_SOURCE_DIR}/modules ${CMAKE_BINARY_DIR}/modules + COMMENT "Copying Python modules to build directory" + ) +endif() + # Handle GUI mode if(ENABLE_GUI) add_definitions(-DENABLE_GUI) @@ -233,8 +252,8 @@ if(ENABLE_GUI) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) - find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Gui Widgets) - find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Gui Widgets) + find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Core Gui Widgets Network) + find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Core Gui Widgets Network) # Add GUI-specific sources list(APPEND GUI_SOURCES @@ -304,6 +323,19 @@ if(ENABLE_GUI) ${CMAKE_SOURCE_DIR}/builder/include/open_converter.h ) + if(BMF_TRANSCODER) + list(APPEND GUI_SOURCES + ${CMAKE_SOURCE_DIR}/component/src/python_install_dialog.cpp + ${CMAKE_SOURCE_DIR}/builder/src/python_manager.cpp + ${CMAKE_SOURCE_DIR}/builder/src/ai_processing_page.cpp + ) + list(APPEND GUI_HEADERS + ${CMAKE_SOURCE_DIR}/component/include/python_install_dialog.h + ${CMAKE_SOURCE_DIR}/builder/include/python_manager.h + ${CMAKE_SOURCE_DIR}/builder/include/ai_processing_page.h + ) + endif() + # Add UI files list(APPEND UI_FILES ${CMAKE_SOURCE_DIR}/builder/src/open_converter.ui @@ -343,6 +375,7 @@ if(ENABLE_GUI) Qt${QT_VERSION_MAJOR}::Core Qt${QT_VERSION_MAJOR}::Gui Qt${QT_VERSION_MAJOR}::Widgets + Qt${QT_VERSION_MAJOR}::Network ) if(${QT_VERSION_MAJOR} GREATER_EQUAL 6) @@ -358,6 +391,73 @@ if(ENABLE_GUI) set_source_files_properties(${APP_ICON_MACOSX} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources") target_sources(OpenConverter PRIVATE ${APP_ICON_MACOSX}) + # Bundle Python modules into app (if BMF is enabled) + if(BMF_TRANSCODER) + # Always bundle Python modules (needed for both Debug and Release) + file(GLOB PYTHON_MODULES "${CMAKE_SOURCE_DIR}/modules/*.py") + foreach(MODULE_FILE ${PYTHON_MODULES}) + set_source_files_properties(${MODULE_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources/modules") + target_sources(OpenConverter PRIVATE ${MODULE_FILE}) + endforeach() + + # Always bundle AI model weights if they exist (for both Debug and Release) + # This allows AI features to work without internet connection + file(GLOB_RECURSE MODEL_WEIGHTS "${CMAKE_SOURCE_DIR}/modules/weights/*.pth") + if(MODEL_WEIGHTS) + message(STATUS "Bundling existing AI model weights (${CMAKE_BUILD_TYPE} mode)") + foreach(WEIGHT_FILE ${MODEL_WEIGHTS}) + set_source_files_properties(${WEIGHT_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources/modules/weights") + target_sources(OpenConverter PRIVATE ${WEIGHT_FILE}) + endforeach() + else() + message(STATUS "No AI model weights found. Run tool/download_models.sh to download them.") + endif() + + # Bundle requirements.txt for Python package installation + set(REQUIREMENTS_FILE "${CMAKE_SOURCE_DIR}/resources/requirements.txt") + if(EXISTS ${REQUIREMENTS_FILE}) + set_source_files_properties(${REQUIREMENTS_FILE} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources") + target_sources(OpenConverter PRIVATE ${REQUIREMENTS_FILE}) + message(STATUS "Bundling requirements.txt for Python package installation") + endif() + + # Check if this is a Release build + if(CMAKE_BUILD_TYPE MATCHES "Release") + message(STATUS "========================================") + message(STATUS "RELEASE BUILD - Library bundling via fix_macos_libs.sh") + message(STATUS "========================================") + message(STATUS "After building, run: ../tool/fix_macos_libs.sh") + message(STATUS "This will bundle Qt, FFmpeg, and BMF libraries") + message(STATUS "========================================") + + # Set rpath to find bundled libraries (Release mode) + # Include Frameworks, Frameworks/lib, and Python.framework + if(APPLE) + set_target_properties(OpenConverter PROPERTIES + BUILD_WITH_INSTALL_RPATH TRUE + INSTALL_RPATH "@executable_path/../Frameworks;@executable_path/../Frameworks/lib;@executable_path/../Frameworks/Python.framework/Versions/Current/lib" + ) + endif() + else() + # Debug mode - minimal bundling + message(STATUS "========================================") + message(STATUS "DEBUG BUILD - Using system libraries") + message(STATUS "========================================") + message(STATUS "Set environment variables:") + message(STATUS " export BMF_ROOT_PATH=${BMF_ROOT_PATH}") + message(STATUS " export PYTHONPATH=${BMF_ROOT_PATH}/lib:${BMF_ROOT_PATH}") + message(STATUS "========================================") + + # Set rpath for development (use system libraries) - macOS only + if(APPLE) + set_target_properties(OpenConverter PROPERTIES + BUILD_WITH_INSTALL_RPATH FALSE + INSTALL_RPATH "@executable_path/../Frameworks;@executable_path/../Frameworks/lib" + ) + endif() + endif() + endif() + set_target_properties(OpenConverter PROPERTIES MACOSX_BUNDLE_GUI_IDENTIFIER com.openconverter.app MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION} diff --git a/src/builder/include/ai_processing_page.h b/src/builder/include/ai_processing_page.h new file mode 100644 index 00000000..2776d927 --- /dev/null +++ b/src/builder/include/ai_processing_page.h @@ -0,0 +1,125 @@ +/* + * Copyright 2025 Jack Lau + * Email: jacklau1222gm@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef AI_PROCESSING_PAGE_H +#define AI_PROCESSING_PAGE_H + +#include "base_page.h" +#include "converter_runner.h" +#include "file_selector_widget.h" +#include "progress_widget.h" +#include "batch_output_widget.h" +#include "batch_mode_helper.h" +#include "bitrate_widget.h" +#include +#include +#include +#include +#include +#include +#include + +class EncodeParameter; +class ProcessParameter; + +class AIProcessingPage : public BasePage { + Q_OBJECT + +public: + explicit AIProcessingPage(QWidget *parent = nullptr); + ~AIProcessingPage() override; + + void OnPageActivated() override; + void OnPageDeactivated() override; + QString GetPageTitle() const override { return "AI Processing"; } + void RetranslateUi() override; + +protected: + void OnInputFileChanged(const QString &newPath) override; + void OnOutputPathUpdate() override; + +private slots: + void OnInputFileSelected(const QString &filePath); + void OnOutputFileSelected(const QString &filePath); + void OnAlgorithmChanged(int index); + void OnFormatChanged(int index); + void OnProcessClicked(); + void OnProcessFinished(bool success); + +signals: + void ProcessComplete(bool success); + +private: + void SetupUI(); + void UpdateOutputPath(); + QString GetFileExtension(const QString &filePath); + EncodeParameter* CreateEncodeParameter(); + + // Input/Output section + FileSelectorWidget *inputFileSelector; + FileSelectorWidget *outputFileSelector; + + // Batch output widget (shown when batch files selected) + BatchOutputWidget *batchOutputWidget; + + // Algorithm selection section + QGroupBox *algorithmGroupBox; + QLabel *algorithmLabel; + QComboBox *algorithmComboBox; + + // Algorithm settings section (dynamic based on selected algorithm) + QGroupBox *algoSettingsGroupBox; + QStackedWidget *algoSettingsStack; + + // Upscaler settings widget + QWidget *upscalerSettingsWidget; + QLabel *upscaleFactorLabel; + QSpinBox *upscaleFactorSpinBox; + + // Video settings section + QGroupBox *videoGroupBox; + QLabel *videoCodecLabel; + QComboBox *videoCodecComboBox; + QLabel *videoBitrateLabel; + BitrateWidget *videoBitrateWidget; + + // Audio settings section + QGroupBox *audioGroupBox; + QLabel *audioCodecLabel; + QComboBox *audioCodecComboBox; + QLabel *audioBitrateLabel; + BitrateWidget *audioBitrateWidget; + + // Format section + QGroupBox *formatGroupBox; + QLabel *formatLabel; + QComboBox *formatComboBox; + + // Progress section + ProgressWidget *progressWidget; + + // Action section + QPushButton *processButton; + + // Conversion runner + ConverterRunner *converterRunner; + + // Batch mode helper + BatchModeHelper *batchModeHelper; +}; + +#endif // AI_PROCESSING_PAGE_H diff --git a/src/builder/include/open_converter.h b/src/builder/include/open_converter.h index 1df13ff0..7b396d10 100644 --- a/src/builder/include/open_converter.h +++ b/src/builder/include/open_converter.h @@ -43,6 +43,7 @@ #include #include #include +#include #include #include @@ -88,6 +89,7 @@ class OpenConverter : public QMainWindow, public ProcessObserver { private slots: void SlotLanguageChanged(QAction *action); void SlotTranscoderChanged(QAction *action); + void SlotPythonChanged(QAction *action); void OnNavigationButtonClicked(int pageIndex); void OnQueueButtonClicked(); @@ -107,10 +109,16 @@ private slots: QMessageBox *displayResult; QActionGroup *transcoderGroup; QActionGroup *languageGroup; + QActionGroup *pythonGroup; + QString customPythonPath; // Navigation and page management QButtonGroup *navButtonGroup; QList pages; + QList navButtons; + QLabel *labelCommonSection; + QLabel *labelAdvancedSection; + QPushButton *queueButton; SharedData *sharedData; BatchQueueDialog *batchQueueDialog; @@ -121,6 +129,7 @@ private slots: QString FormatFrequency(int64_t hertz); // Page management methods + void SetupNavigationButtons(); void InitializePages(); void SwitchToPage(int pageIndex); @@ -130,6 +139,12 @@ private slots: // Get current transcoder name QString GetCurrentTranscoderName() const; + + // Get current Python path setting + QString GetPythonSitePackagesPath() const; + + // Static method for transcoder_bmf to get Python path + static QString GetStoredPythonPath(); }; #endif // OPEN_CONVERTER_H diff --git a/src/builder/include/python_manager.h b/src/builder/include/python_manager.h new file mode 100644 index 00000000..5761185b --- /dev/null +++ b/src/builder/include/python_manager.h @@ -0,0 +1,175 @@ +/* + * Copyright 2025 Jack Lau + * Email: jacklau1222gm@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef PYTHON_MANAGER_H +#define PYTHON_MANAGER_H + +#include +#include +#include +#include +#include + +/** + * @brief Manages embedded Python runtime for OpenConverter + * + * This class handles: + * - Detecting if Python is installed in app bundle + * - Downloading Python 3.9 standalone build + * - Installing Python packages from requirements.txt + * - Providing isolated Python environment (no system conflicts) + * + * Similar to how Blender, GIMP, and other apps bundle Python. + */ +class PythonManager : public QObject { + Q_OBJECT + +public: + enum class Status { + NotInstalled, // Python not found in app bundle + Installing, // Currently downloading/installing + Installed, // Python installed and ready + Error // Installation failed + }; + + explicit PythonManager(QObject *parent = nullptr); + ~PythonManager(); + + /** + * @brief Check if Python is installed in app bundle + * @return true if Python.framework exists and is functional + */ + bool IsPythonInstalled(); + + /** + * @brief Check if all required packages are installed + * @return true if all packages from requirements.txt are available + */ + bool ArePackagesInstalled(); + + /** + * @brief Check if system Python 3.9 with required packages exists + * @return true if system Python 3.9 has all required packages (Debug mode optimization) + */ + bool CheckSystemPython(); + + /** + * @brief Get path to embedded Python executable + * @return Path to python3 binary, or empty string if not installed + */ + QString GetPythonPath(); + + /** + * @brief Get path to site-packages directory + * @return Path to site-packages, or empty string if not installed + */ + QString GetSitePackagesPath(); + + /** + * @brief Get current installation status + */ + Status GetStatus() const { return status; } + + /** + * @brief Get installation progress (0-100) + */ + int GetProgress() const { return progress; } + + /** + * @brief Get current status message + */ + QString GetStatusMessage() const { return statusMessage; } + +public slots: + /** + * @brief Download and install Python 3.9 to app bundle + * + * Downloads Python standalone build from python.org + * Extracts to Contents/Frameworks/Python.framework + * Emits signals for progress updates + */ + void InstallPython(); + + /** + * @brief Install packages from requirements.txt + * + * Uses bundled pip to install packages + * Packages are installed to embedded site-packages + * Does not affect system Python + */ + void InstallPackages(); + + /** + * @brief Cancel ongoing installation + */ + void CancelInstallation(); + +signals: + /** + * @brief Emitted when installation status changes + */ + void StatusChanged(PythonManager::Status status); + + /** + * @brief Emitted during download/installation + * @param progress Progress percentage (0-100) + * @param message Status message + */ + void ProgressChanged(int progress, const QString &message); + + /** + * @brief Emitted when Python installation completes successfully + */ + void PythonInstalled(); + + /** + * @brief Emitted when package installation completes successfully + */ + void PackagesInstalled(); + + /** + * @brief Emitted when installation fails + * @param error Error message + */ + void InstallationFailed(const QString &error); + +private slots: + void OnDownloadProgress(qint64 bytesReceived, qint64 bytesTotal); + void OnDownloadFinished(); + void OnInstallProcessOutput(); + void OnInstallProcessFinished(int exitCode, QProcess::ExitStatus exitStatus); + +private: + QString GetAppBundlePath(); + QString GetPythonFrameworkPath(); + QString GetRequirementsPath(); + bool ExtractPythonArchive(const QString &archivePath); + bool CopyDirectoryRecursively(const QString &source, const QString &destination); + void SetStatus(Status newStatus, const QString &message); + void SetProgress(int value, const QString &message); + + QNetworkAccessManager *networkManager; + QNetworkReply *currentDownload; + QProcess *installProcess; + + Status status; + int progress; + QString statusMessage; + QString downloadedFilePath; +}; + +#endif // PYTHON_MANAGER_H diff --git a/src/builder/src/ai_processing_page.cpp b/src/builder/src/ai_processing_page.cpp new file mode 100644 index 00000000..9dc56d0c --- /dev/null +++ b/src/builder/src/ai_processing_page.cpp @@ -0,0 +1,460 @@ +/* + * Copyright 2025 Jack Lau + * Email: jacklau1222gm@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "../include/ai_processing_page.h" +#include "../include/open_converter.h" +#include "../include/shared_data.h" +#include "../include/batch_queue.h" +#include "../include/batch_item.h" +#include "../include/transcoder_helper.h" +#include "../include/python_manager.h" +#include "../../common/include/encode_parameter.h" +#include "../../common/include/process_parameter.h" +#include "../../engine/include/converter.h" +#include "../../component/include/python_install_dialog.h" +#include +#include +#include +#include +#include + +AIProcessingPage::AIProcessingPage(QWidget *parent) : BasePage(parent), converterRunner(nullptr) { + SetupUI(); +} + +AIProcessingPage::~AIProcessingPage() { +} + +void AIProcessingPage::OnPageActivated() { + BasePage::OnPageActivated(); + HandleSharedDataUpdate(inputFileSelector->GetLineEdit(), outputFileSelector->GetLineEdit(), + GetFileExtension(inputFileSelector->GetFilePath())); + + // Check if Python is installed for AI Processing + // In Debug mode, skip installation dialog (assume developer has configured environment) +#if defined(NDEBUG) || defined(__linux__) + // Release mode: check Python and offer installation + PythonManager pythonManager; + + // Check status: embedded Python, system Python, or not installed + if (pythonManager.GetStatus() != PythonManager::Status::Installed) { + // Python not available (neither embedded nor system) + // Show installation dialog + QMessageBox::StandardButton reply = QMessageBox::question( + this, + tr("Python Required"), + tr("AI Processing requires Python 3.9 and additional packages.\n\n" + "Would you like to download and install them now?\n" + "(Download size: ~550 MB, completely isolated from system Python)"), + QMessageBox::Yes | QMessageBox::No + ); + + if (reply == QMessageBox::Yes) { + PythonInstallDialog dialog(this); + if (dialog.exec() != QDialog::Accepted) { + // User cancelled installation + QMessageBox::information( + this, + tr("AI Processing Unavailable"), + tr("AI Processing features require Python to be installed.\n\n" + "You can install it later by returning to this page.") + ); + } + } + } +#else + // Debug mode: assume developer has configured Python environment + // Skip installation dialog + qDebug() << "Debug mode: Skipping Python installation check (assuming developer environment)"; +#endif +} + +void AIProcessingPage::OnInputFileChanged(const QString &newPath) { + QString ext = GetFileExtension(newPath); + if (!ext.isEmpty()) { + int index = formatComboBox->findText(ext); + if (index >= 0) { + formatComboBox->setCurrentIndex(index); + } + } + // Update output path when input changes + UpdateOutputPath(); +} + +void AIProcessingPage::OnOutputPathUpdate() { + UpdateOutputPath(); +} + +void AIProcessingPage::OnPageDeactivated() { + BasePage::OnPageDeactivated(); + HandleSharedDataUpdate(inputFileSelector->GetLineEdit(), outputFileSelector->GetLineEdit(), + formatComboBox->currentText()); +} + +void AIProcessingPage::SetupUI() { + QVBoxLayout *mainLayout = new QVBoxLayout(this); + mainLayout->setSpacing(15); + mainLayout->setContentsMargins(20, 20, 20, 20); + + // Input File Selector (with Batch button) + inputFileSelector = new FileSelectorWidget( + tr("Input File"), + FileSelectorWidget::InputFile, + tr("Select a media file or click Batch for multiple files..."), + tr("All Files (*.*)"), + tr("Select Media File"), + this + ); + connect(inputFileSelector, &FileSelectorWidget::FileSelected, this, &AIProcessingPage::OnInputFileSelected); + mainLayout->addWidget(inputFileSelector); + + // Algorithm Selection Section + algorithmGroupBox = new QGroupBox(tr("Algorithm"), this); + QGridLayout *algorithmLayout = new QGridLayout(algorithmGroupBox); + algorithmLayout->setSpacing(10); + + algorithmLabel = new QLabel(tr("Select Algorithm:"), algorithmGroupBox); + algorithmComboBox = new QComboBox(algorithmGroupBox); + algorithmComboBox->addItem(tr("Upscaler")); + algorithmComboBox->setCurrentIndex(0); + connect(algorithmComboBox, QOverload::of(&QComboBox::currentIndexChanged), + this, &AIProcessingPage::OnAlgorithmChanged); + + algorithmLayout->addWidget(algorithmLabel, 0, 0); + algorithmLayout->addWidget(algorithmComboBox, 0, 1); + + mainLayout->addWidget(algorithmGroupBox); + + // Algorithm Settings Section (dynamic based on selected algorithm) + algoSettingsGroupBox = new QGroupBox(tr("Algorithm Settings"), this); + QGridLayout *algoSettingsLayout = new QGridLayout(algoSettingsGroupBox); + algoSettingsLayout->setSpacing(10); + + algoSettingsStack = new QStackedWidget(algoSettingsGroupBox); + algoSettingsStack->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); + + // Upscaler Settings Widget + upscalerSettingsWidget = new QWidget(algoSettingsStack); + QGridLayout *upscalerLayout = new QGridLayout(upscalerSettingsWidget); + upscalerLayout->setSpacing(10); + upscalerLayout->setContentsMargins(0, 0, 0, 0); + + upscaleFactorLabel = new QLabel(tr("Upscale Factor:"), upscalerSettingsWidget); + upscaleFactorSpinBox = new QSpinBox(upscalerSettingsWidget); + upscaleFactorSpinBox->setMinimum(2); + upscaleFactorSpinBox->setMaximum(8); + upscaleFactorSpinBox->setValue(2); + upscaleFactorSpinBox->setSuffix("x"); + + upscalerLayout->addWidget(upscaleFactorLabel, 0, 0); + upscalerLayout->addWidget(upscaleFactorSpinBox, 0, 1); + + algoSettingsStack->addWidget(upscalerSettingsWidget); + + algoSettingsLayout->addWidget(algoSettingsStack, 0, 0, 1, 2); + mainLayout->addWidget(algoSettingsGroupBox); + + // Video Settings Section + videoGroupBox = new QGroupBox(tr("Video Settings"), this); + QGridLayout *videoLayout = new QGridLayout(videoGroupBox); + videoLayout->setSpacing(10); + + videoCodecLabel = new QLabel(tr("Codec:"), videoGroupBox); + videoCodecComboBox = new QComboBox(videoGroupBox); + videoCodecComboBox->addItems({"auto", "libx264", "libx265", "libvpx-vp9", "copy"}); + videoCodecComboBox->setCurrentText("auto"); + + videoBitrateLabel = new QLabel(tr("Bitrate:"), videoGroupBox); + videoBitrateWidget = new BitrateWidget(BitrateWidget::Video, videoGroupBox); + + videoLayout->addWidget(videoCodecLabel, 0, 0); + videoLayout->addWidget(videoCodecComboBox, 0, 1); + videoLayout->addWidget(videoBitrateLabel, 1, 0); + videoLayout->addWidget(videoBitrateWidget, 1, 1); + + mainLayout->addWidget(videoGroupBox); + + // Audio Settings Section + audioGroupBox = new QGroupBox(tr("Audio Settings"), this); + QGridLayout *audioLayout = new QGridLayout(audioGroupBox); + audioLayout->setSpacing(10); + + audioCodecLabel = new QLabel(tr("Codec:"), audioGroupBox); + audioCodecComboBox = new QComboBox(audioGroupBox); + audioCodecComboBox->addItems({"auto", "aac", "libmp3lame", "libopus", "copy"}); + audioCodecComboBox->setCurrentText("auto"); + + audioBitrateLabel = new QLabel(tr("Bitrate:"), audioGroupBox); + audioBitrateWidget = new BitrateWidget(BitrateWidget::Audio, audioGroupBox); + + audioLayout->addWidget(audioCodecLabel, 0, 0); + audioLayout->addWidget(audioCodecComboBox, 0, 1); + audioLayout->addWidget(audioBitrateLabel, 1, 0); + audioLayout->addWidget(audioBitrateWidget, 1, 1); + + mainLayout->addWidget(audioGroupBox); + + // Format Section + formatGroupBox = new QGroupBox(tr("File Format"), this); + QHBoxLayout *formatLayout = new QHBoxLayout(formatGroupBox); + + formatLabel = new QLabel(tr("Format:"), formatGroupBox); + formatComboBox = new QComboBox(formatGroupBox); + formatComboBox->addItems({"mp4", "mkv", "avi", "mov", "flv", "webm", "ts", "jpg", "png"}); + formatComboBox->setCurrentText("mp4"); + connect(formatComboBox, QOverload::of(&QComboBox::currentIndexChanged), + this, &AIProcessingPage::OnFormatChanged); + + formatLayout->addWidget(formatLabel); + formatLayout->addWidget(formatComboBox); + formatLayout->addStretch(); + + mainLayout->addWidget(formatGroupBox); + + // Output File Selector + outputFileSelector = new FileSelectorWidget( + tr("Output File"), + FileSelectorWidget::OutputFile, + tr("Output file path..."), + tr("All Files (*.*)"), + tr("Select Output File"), + this + ); + connect(outputFileSelector, &FileSelectorWidget::FileSelected, this, &AIProcessingPage::OnOutputFileSelected); + mainLayout->addWidget(outputFileSelector); + + // Batch Output Widget (hidden by default, shown when batch files selected) + batchOutputWidget = new BatchOutputWidget(this); + batchOutputWidget->setVisible(false); + mainLayout->addWidget(batchOutputWidget); + + // Process Button + processButton = new QPushButton(tr("Process / Add to Queue"), this); + processButton->setEnabled(false); + processButton->setMinimumHeight(40); + connect(processButton, &QPushButton::clicked, this, &AIProcessingPage::OnProcessClicked); + mainLayout->addWidget(processButton); + + // Progress Section (placed after button to avoid blank space when hidden) + progressWidget = new ProgressWidget(this); + mainLayout->addWidget(progressWidget); + + // Initialize converter runner + converterRunner = new ConverterRunner( + progressWidget->GetProgressBar(), progressWidget->GetProgressLabel(), processButton, + tr("Processing..."), tr("Process / Add to Queue"), + tr("Success"), tr("AI processing completed successfully!"), + tr("Error"), tr("Failed to process file."), + this + ); + connect(converterRunner, &ConverterRunner::ConversionFinished, this, &AIProcessingPage::OnProcessFinished); + + // Initialize batch mode helper + batchModeHelper = new BatchModeHelper( + inputFileSelector, batchOutputWidget, processButton, + tr("Process / Add to Queue"), tr("Add to Queue"), this + ); + batchModeHelper->SetSingleOutputWidget(outputFileSelector); + batchModeHelper->SetEncodeParameterCreator([this]() { + return CreateEncodeParameter(); + }); +} + +void AIProcessingPage::OnInputFileSelected(const QString &filePath) { + if (filePath.isEmpty()) { + return; + } + + // Update shared input file path + OpenConverter *mainWindow = qobject_cast(window()); + if (mainWindow && mainWindow->GetSharedData()) { + mainWindow->GetSharedData()->SetInputFilePath(filePath); + } + + // Set default format to same as input file + QString ext = GetFileExtension(filePath); + if (!ext.isEmpty()) { + int index = formatComboBox->findText(ext); + if (index >= 0) { + formatComboBox->setCurrentIndex(index); + } + } + + // Update output path + UpdateOutputPath(); +} + +void AIProcessingPage::OnOutputFileSelected(const QString &filePath) { + // Mark output path as manually set + OpenConverter *mainWindow = qobject_cast(window()); + if (mainWindow && mainWindow->GetSharedData()) { + mainWindow->GetSharedData()->SetOutputFilePath(filePath); + } +} + +void AIProcessingPage::OnAlgorithmChanged(int index) { + // Switch to the corresponding settings widget + algoSettingsStack->setCurrentIndex(index); +} + +void AIProcessingPage::OnFormatChanged(int index) { + Q_UNUSED(index); + UpdateOutputPath(); +} + +void AIProcessingPage::OnProcessClicked() { + // Check if batch mode is active + if (batchModeHelper->IsBatchMode()) { + // Batch mode: Add to queue + QString format = formatComboBox->currentText(); + batchModeHelper->AddToQueue(format); + return; + } + + // Single file mode: Process immediately + QString inputPath = inputFileSelector->GetFilePath(); + QString outputPath = outputFileSelector->GetFilePath(); + + if (inputPath.isEmpty()) { + QMessageBox::warning(this, tr("Warning"), tr("Please select an input file.")); + return; + } + + if (outputPath.isEmpty()) { + QMessageBox::warning(this, tr("Warning"), tr("Please select an output file.")); + return; + } + + // Create parameters + EncodeParameter *encodeParam = CreateEncodeParameter(); + ProcessParameter *processParam = new ProcessParameter(); + + // Get transcoder name (must be BMF for AI processing) + QString transcoderName = "BMF"; + + // Run conversion using ConverterRunner + converterRunner->RunConversion(inputPath, outputPath, encodeParam, processParam, transcoderName); +} + +void AIProcessingPage::OnProcessFinished(bool success) { + Q_UNUSED(success); + // ConverterRunner handles all UI updates and message boxes + // This slot is kept for potential custom post-processing + emit ProcessComplete(success); +} + +void AIProcessingPage::UpdateOutputPath() { + QString inputPath = inputFileSelector->GetFilePath(); + if (!inputPath.isEmpty()) { + OpenConverter *mainWindow = qobject_cast(window()); + if (mainWindow && mainWindow->GetSharedData()) { + QString format = formatComboBox->currentText(); + QString outputPath = mainWindow->GetSharedData()->GenerateOutputPath(format); + outputFileSelector->SetFilePath(outputPath); + processButton->setEnabled(true); + } + } +} + +QString AIProcessingPage::GetFileExtension(const QString &filePath) { + QFileInfo fileInfo(filePath); + return fileInfo.suffix(); +} + +EncodeParameter* AIProcessingPage::CreateEncodeParameter() { + EncodeParameter *encodeParam = new EncodeParameter(); + + // Set algorithm mode based on selected algorithm + int algoIndex = algorithmComboBox->currentIndex(); + if (algoIndex == 0) { // Upscaler + encodeParam->set_algo_mode(AlgoMode::Upscale); + encodeParam->set_upscale_factor(upscaleFactorSpinBox->value()); + } + + // Set video codec and bitrate + QString videoCodec = videoCodecComboBox->currentText(); + if (videoCodec != "auto") + encodeParam->set_video_codec_name(videoCodec.toStdString()); + + int videoBitrate = videoBitrateWidget->GetBitrate(); + if (videoBitrate > 0) { + encodeParam->set_video_bit_rate(videoBitrate); + } + + // Set audio codec and bitrate + QString audioCodec = audioCodecComboBox->currentText(); + if (audioCodec != "auto") + encodeParam->set_audio_codec_name(audioCodec.toStdString()); + + int audioBitrate = audioBitrateWidget->GetBitrate(); + if (audioBitrate > 0) { + encodeParam->set_audio_bit_rate(audioBitrate); + } + + return encodeParam; +} + +void AIProcessingPage::RetranslateUi() { + // Update input file selector + inputFileSelector->setTitle(tr("Input File")); + inputFileSelector->SetPlaceholder(tr("Select a media file or click Batch for multiple files...")); + inputFileSelector->RetranslateUi(); + + // Update algorithm section + algorithmGroupBox->setTitle(tr("Algorithm")); + algorithmLabel->setText(tr("Select Algorithm:")); + algorithmComboBox->setItemText(0, tr("Upscaler")); + + // Update algorithm settings section + algoSettingsGroupBox->setTitle(tr("Algorithm Settings")); + upscaleFactorLabel->setText(tr("Upscale Factor:")); + + // Update video settings section + videoGroupBox->setTitle(tr("Video Settings")); + videoCodecLabel->setText(tr("Codec:")); + videoBitrateLabel->setText(tr("Bitrate:")); + videoBitrateWidget->RetranslateUi(); + + // Update audio settings section + audioGroupBox->setTitle(tr("Audio Settings")); + audioCodecLabel->setText(tr("Codec:")); + audioBitrateLabel->setText(tr("Bitrate:")); + audioBitrateWidget->RetranslateUi(); + + // Update format section + formatGroupBox->setTitle(tr("File Format")); + formatLabel->setText(tr("Format:")); + + // Update output file selector + outputFileSelector->setTitle(tr("Output File")); + outputFileSelector->SetPlaceholder(tr("Output file path...")); + outputFileSelector->RetranslateUi(); + + // Update batch output widget + batchOutputWidget->RetranslateUi(); + + // Update button text based on batch mode + if (batchModeHelper) { + if (inputFileSelector->IsBatchMode()) { + processButton->setText(tr("Add to Queue")); + } else { + processButton->setText(tr("Process / Add to Queue")); + } + } else { + processButton->setText(tr("Process")); + } +} diff --git a/src/builder/src/converter_runner.cpp b/src/builder/src/converter_runner.cpp index 00e71bc8..6857693c 100644 --- a/src/builder/src/converter_runner.cpp +++ b/src/builder/src/converter_runner.cpp @@ -113,6 +113,12 @@ bool ConverterRunner::RunConversion(const QString &inputPath, delete processParam; }); + // Set larger stack size for BMF operations (Python/numpy needs more stack) + // Default is 512KB on macOS, increase to 8MB for AI processing + if (transcoderName == "BMF") { + thread->setStackSize(8 * 1024 * 1024); // 8 MB + } + connect(thread, &QThread::finished, thread, &QThread::deleteLater); thread->start(); diff --git a/src/builder/src/open_converter.cpp b/src/builder/src/open_converter.cpp index b5119b72..8485c577 100644 --- a/src/builder/src/open_converter.cpp +++ b/src/builder/src/open_converter.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -40,6 +41,7 @@ #include #include #include +#include #include "../../common/include/encode_parameter.h" #include "../../common/include/info.h" @@ -58,6 +60,7 @@ #include "../include/remux_page.h" #include "../include/shared_data.h" #include "../include/transcode_page.h" +#include "../include/ai_processing_page.h" #include "ui_open_converter.h" #include @@ -130,6 +133,27 @@ OpenConverter::OpenConverter(QWidget *parent) languageGroup->addAction(action); } + // Setup Python menu + pythonGroup = new QActionGroup(this); + pythonGroup->setExclusive(true); + QList pythonActions = ui->menuPython->actions(); + for (QAction* action : pythonActions) { + action->setCheckable(true); + pythonGroup->addAction(action); + } + + // Load saved Python setting or default to App Python + QSettings settings("OpenConverter", "OpenConverter"); + QString savedPython = settings.value("python/mode", "pythonAppSupport").toString(); + customPythonPath = settings.value("python/customPath", "").toString(); + + for (QAction* action : pythonActions) { + if (action->objectName() == savedPython) { + action->setChecked(true); + break; + } + } + // Initialize language - default to English (no translation file needed) m_currLang = "english"; m_langPath = ":/"; @@ -144,13 +168,9 @@ OpenConverter::OpenConverter(QWidget *parent) // Initialize navigation button group navButtonGroup = new QButtonGroup(this); - navButtonGroup->addButton(ui->btnInfoView, 0); - navButtonGroup->addButton(ui->btnCompressPicture, 1); - navButtonGroup->addButton(ui->btnExtractAudio, 2); - navButtonGroup->addButton(ui->btnCutVideo, 3); - navButtonGroup->addButton(ui->btnCreateGif, 4); - navButtonGroup->addButton(ui->btnRemux, 5); - navButtonGroup->addButton(ui->btnTranscode, 6); + + // Setup navigation buttons dynamically + SetupNavigationButtons(); // Connect navigation button group connect(navButtonGroup, QOverload::of(&QButtonGroup::idClicked), @@ -160,8 +180,8 @@ OpenConverter::OpenConverter(QWidget *parent) InitializePages(); // Set first page as active - if (!pages.isEmpty()) { - ui->btnInfoView->setChecked(true); + if (!pages.isEmpty() && !navButtons.isEmpty()) { + navButtons.first()->setChecked(true); SwitchToPage(0); } @@ -171,8 +191,8 @@ OpenConverter::OpenConverter(QWidget *parent) connect(ui->menuTranscoder, SIGNAL(triggered(QAction *)), this, SLOT(SlotTranscoderChanged(QAction *))); - // Connect Queue button - connect(ui->queueButton, &QPushButton::clicked, this, &OpenConverter::OnQueueButtonClicked); + connect(ui->menuPython, SIGNAL(triggered(QAction *)), this, + SLOT(SlotPythonChanged(QAction *))); } void OpenConverter::dragEnterEvent(QDragEnterEvent *event) { @@ -235,6 +255,48 @@ void OpenConverter::SlotTranscoderChanged(QAction *action) { } } +// Called every time, when a menu entry of the Python menu is called +void OpenConverter::SlotPythonChanged(QAction *action) { + if (!action) return; + + QString pythonMode = action->objectName(); + QSettings settings("OpenConverter", "OpenConverter"); + + if (pythonMode == "pythonCustom") { + // Show file dialog to select site-packages path + QString dir = QFileDialog::getExistingDirectory( + this, + tr("Select Python site-packages Directory"), + customPythonPath.isEmpty() ? "/opt/homebrew/lib/python3.9/site-packages" : customPythonPath, + QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks + ); + + if (!dir.isEmpty()) { + customPythonPath = dir; + settings.setValue("python/mode", pythonMode); + settings.setValue("python/customPath", customPythonPath); + ui->statusBar->showMessage( + tr("Python path set to: %1").arg(customPythonPath)); + } else { + // User cancelled, revert to previous selection + QString savedPython = settings.value("python/mode", "pythonAppSupport").toString(); + QList pythonActions = ui->menuPython->actions(); + for (QAction* act : pythonActions) { + if (act->objectName() == savedPython) { + act->setChecked(true); + break; + } + } + return; + } + } else { + settings.setValue("python/mode", pythonMode); + if (pythonMode == "pythonAppSupport") { + ui->statusBar->showMessage(tr("Using App Python")); + } + } +} + // Called every time, when a menu entry of the language menu is called void OpenConverter::SlotLanguageChanged(QAction *action) { if (0 != action) { @@ -279,6 +341,35 @@ void OpenConverter::changeEvent(QEvent *event) { if (event->type() == QEvent::LanguageChange) { ui->retranslateUi(this); + // Update navigation labels and buttons + if (labelCommonSection) { + labelCommonSection->setText(tr("COMMON")); + } + if (labelAdvancedSection) { + labelAdvancedSection->setText(tr("ADVANCED")); + } + if (queueButton) { + queueButton->setText(tr("📋 Queue")); + queueButton->setToolTip(tr("View batch processing queue")); + } + + // Update navigation button texts + QStringList buttonTexts = { + tr("Info View"), + tr("Compress Picture"), + tr("Extract Audio"), + tr("Cut Video"), + tr("Create GIF"), + tr("Remux"), + tr("Transcode") + }; +#if defined(ENABLE_BMF) && defined(ENABLE_GUI) + buttonTexts.append(tr("AI Processing")); +#endif + for (int i = 0; i < navButtons.size() && i < buttonTexts.size(); ++i) { + navButtons[i]->setText(buttonTexts[i]); + } + // Update language in all pages for (BasePage *page : pages) { if (page) { @@ -331,6 +422,57 @@ void OpenConverter::InfoDisplay(QuickInfo *quickInfo) { // This can be implemented later for displaying info in pages } +void OpenConverter::SetupNavigationButtons() { + QVBoxLayout *navLayout = ui->navVerticalLayout; + + // Helper lambda to create navigation buttons + auto createNavButton = [this](const QString &text, int index) -> QPushButton* { + QPushButton *btn = new QPushButton(text, ui->leftNavWidget); + btn->setCheckable(true); + navButtonGroup->addButton(btn, index); + navButtons.append(btn); + return btn; + }; + + int pageIndex = 0; + + // COMMON section label + labelCommonSection = new QLabel(tr("COMMON"), ui->leftNavWidget); + navLayout->addWidget(labelCommonSection); + + // Common pages - always visible + navLayout->addWidget(createNavButton(tr("Info View"), pageIndex++)); + navLayout->addWidget(createNavButton(tr("Compress Picture"), pageIndex++)); + navLayout->addWidget(createNavButton(tr("Extract Audio"), pageIndex++)); + navLayout->addWidget(createNavButton(tr("Cut Video"), pageIndex++)); + navLayout->addWidget(createNavButton(tr("Create GIF"), pageIndex++)); + + // ADVANCED section label + labelAdvancedSection = new QLabel(tr("ADVANCED"), ui->leftNavWidget); + navLayout->addWidget(labelAdvancedSection); + + // Advanced pages + navLayout->addWidget(createNavButton(tr("Remux"), pageIndex++)); + navLayout->addWidget(createNavButton(tr("Transcode"), pageIndex++)); + +#if defined(ENABLE_BMF) && defined(ENABLE_GUI) + // AI Processing page - only when BMF is enabled + navLayout->addWidget(createNavButton(tr("AI Processing"), pageIndex++)); +#endif + + // Add spacer to push queue button to bottom + navLayout->addStretch(); + + // Queue button (not part of navigation group) + queueButton = new QPushButton(tr("📋 Queue"), ui->leftNavWidget); + queueButton->setCheckable(false); + queueButton->setToolTip(tr("View batch processing queue")); + navLayout->addWidget(queueButton); + + // Connect Queue button + connect(queueButton, &QPushButton::clicked, this, &OpenConverter::OnQueueButtonClicked); +} + void OpenConverter::InitializePages() { // Create pages for each navigation item // Common section @@ -342,6 +484,9 @@ void OpenConverter::InitializePages() { // Advanced section pages.append(new RemuxPage(this)); pages.append(new TranscodePage(this)); +#if defined(ENABLE_BMF) && defined(ENABLE_GUI) + pages.append(new AIProcessingPage(this)); +#endif // Add all pages to the stacked widget for (BasePage *page : pages) { @@ -422,4 +567,35 @@ QString OpenConverter::GetCurrentTranscoderName() const { return "FFMPEG"; } +QString OpenConverter::GetPythonSitePackagesPath() const { + QAction *checkedAction = pythonGroup->checkedAction(); + if (checkedAction) { + QString mode = checkedAction->objectName(); + if (mode == "pythonCustom" && !customPythonPath.isEmpty()) { + return customPythonPath; + } else if (mode == "pythonAppSupport") { + // Python installed in ~/Library/Application Support/OpenConverter/ + QString appSupportPath = QDir::homePath() + + "/Library/Application Support/OpenConverter/Python.framework/lib/python3.9/site-packages"; + return appSupportPath; + } + } + // Default: Bundled or empty (transcoder will use bundled) + return QString(); +} + +QString OpenConverter::GetStoredPythonPath() { + // Static method that can be called from transcoder_bmf without GUI instance + QSettings settings("OpenConverter", "OpenConverter"); + QString pythonMode = settings.value("python/mode", "pythonAppSupport").toString(); + + if (pythonMode == "pythonCustom") { + return settings.value("python/customPath", "").toString(); + } + // Default to App Python (~/Library/Application Support/OpenConverter/) + QString appSupportPath = QDir::homePath() + + "/Library/Application Support/OpenConverter/Python.framework/lib/python3.9/site-packages"; + return appSupportPath; +} + #include "open_converter.moc" diff --git a/src/builder/src/open_converter.ui b/src/builder/src/open_converter.ui index eb39f4af..62c10ffd 100644 --- a/src/builder/src/open_converter.ui +++ b/src/builder/src/open_converter.ui @@ -91,116 +91,7 @@ QLabel { 0 - - - - COMMON - - - - - - - Info View - - - true - - - - - - - Compress Picture - - - true - - - - - - - Extract Audio - - - true - - - - - - - Cut Video - - - true - - - - - - - Create GIF - - - true - - - - - - - ADVANCED - - - - - - - Remux - - - true - - - - - - - Transcode - - - true - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - 📋 Queue - - - false - - - View batch processing queue - - - + @@ -255,8 +146,16 @@ QLabel { Transcoder + + + Python + + + + + @@ -275,6 +174,23 @@ QLabel { Chinese + + + + pythonAppSupport + + + App Python + + + + + pythonCustom + + + Custom Path... + + diff --git a/src/builder/src/python_manager.cpp b/src/builder/src/python_manager.cpp new file mode 100644 index 00000000..734e715e --- /dev/null +++ b/src/builder/src/python_manager.cpp @@ -0,0 +1,576 @@ +/* + * Copyright 2025 Jack Lau + * Email: jacklau1222gm@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "python_manager.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#ifdef __APPLE__ +#include +#elif defined(__linux__) +#include +#endif + +// Python 3.9 standalone build URL (macOS) +// Using Python Standalone Builds from Astral (formerly Gregory Szorc) +// These are pre-built, relocatable Python frameworks - much easier to extract +#ifdef __APPLE__ +#ifdef __aarch64__ +// macOS ARM64 (Apple Silicon) +const QString PYTHON_DOWNLOAD_URL = "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.9.25+20251031-aarch64-apple-darwin-install_only.tar.gz"; +const QString PYTHON_ARCHIVE_SIZE_MB = "18"; +#else +// macOS x86_64 (Intel) +const QString PYTHON_DOWNLOAD_URL = "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.9.25+20251031-x86_64-apple-darwin-install_only.tar.gz"; +const QString PYTHON_ARCHIVE_SIZE_MB = "18"; +#endif +#endif + +#ifdef __linux__ +#ifdef __aarch64__ +const QString PYTHON_DOWNLOAD_URL = "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.9.25+20251031-aarch64-unknown-linux-gnu-install_only.tar.gz"; +const QString PYTHON_ARCHIVE_SIZE_MB = "18"; +#else +const QString PYTHON_DOWNLOAD_URL = "https://github.com/astral-sh/python-build-standalone/releases/download/20251031/cpython-3.9.25+20251031-x86_64-unknown-linux-gnu-install_only.tar.gz"; +const QString PYTHON_ARCHIVE_SIZE_MB = "18"; +#endif +#endif + +PythonManager::PythonManager(QObject *parent) + : QObject(parent) + , networkManager(new QNetworkAccessManager(this)) + , currentDownload(nullptr) + , installProcess(nullptr) + , status(Status::NotInstalled) + , progress(0) +{ + // Check initial status + // First check embedded Python in app bundle + if (IsPythonInstalled()) { + if (ArePackagesInstalled()) { + SetStatus(Status::Installed, "Python and packages are ready"); + } else { + SetStatus(Status::NotInstalled, "Python installed but packages missing"); + } + } else { +#if !defined(NDEBUG) && !defined(__linux__) + // Debug mode on macOS/Windows only: check if system Python 3.9 exists + // This allows developers to use their existing Python environment + if (CheckSystemPython()) { + SetStatus(Status::Installed, "Using system Python 3.9 with required packages"); + } else { + SetStatus(Status::NotInstalled, "Python not installed"); + } +#else + // Release mode OR Linux: Only use App Python, never fall back to system Python + SetStatus(Status::NotInstalled, "Python not installed"); +#endif + } +} + +PythonManager::~PythonManager() { + if (currentDownload) { + currentDownload->abort(); + currentDownload->deleteLater(); + } + if (installProcess) { + installProcess->kill(); + installProcess->deleteLater(); + } +} + +QString PythonManager::GetAppBundlePath() { +#ifdef __APPLE__ + char path[1024]; + uint32_t size = sizeof(path); + if (_NSGetExecutablePath(path, &size) == 0) { + QString exePath = QString::fromUtf8(path); + // Extract app bundle path (everything before .app/Contents/MacOS) + int appIndex = exePath.indexOf(".app/Contents/MacOS"); + if (appIndex != -1) { + return exePath.left(appIndex + 4); // Include .app + } + } +#endif + return QCoreApplication::applicationDirPath(); +} + +QString PythonManager::GetPythonFrameworkPath() { + // Install embedded Python into Application Support directory so + // the app bundle/installation remains immutable. Use QStandardPaths to get + // the per-user Application Support directory for the app. + // macOS: ~/Library/Application Support/OpenConverter/Python.framework + // Linux: ~/.local/share/OpenConverter/Python.framework + // Windows: C:/Users//AppData/Local/OpenConverter/Python.framework + QString appSupportBase = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + // Ensure directory exists + QDir().mkpath(appSupportBase); + return appSupportBase + "/Python.framework"; +} + +QString PythonManager::GetPythonPath() { + // Python Standalone Builds use flat structure: python/bin/python3.9 + QString pythonPath = GetPythonFrameworkPath() + "/bin/python3.9"; + if (QFile::exists(pythonPath)) { + return pythonPath; + } + return QString(); +} + +QString PythonManager::GetSitePackagesPath() { + // Python Standalone Builds use flat structure: python/lib/python3.9/site-packages + QString sitePath = GetPythonFrameworkPath() + "/lib/python3.9/site-packages"; + if (QDir(sitePath).exists()) { + return sitePath; + } + return QString(); +} + +QString PythonManager::GetRequirementsPath() { +#ifdef __APPLE__ + // macOS: App bundle structure + return GetAppBundlePath() + "/Contents/Resources/requirements.txt"; +#else + // Linux: requirements.txt is in the same directory as the executable + // or in a resources subdirectory + QString appDir = GetAppBundlePath(); + + // First try: resources subdirectory + QString resourcesPath = appDir + "/resources/requirements.txt"; + if (QFile::exists(resourcesPath)) { + return resourcesPath; + } + + // Second try: same directory as executable + QString sameDirPath = appDir + "/requirements.txt"; + if (QFile::exists(sameDirPath)) { + return sameDirPath; + } + + // Fallback: return resources path (will show error if not found) + return resourcesPath; +#endif +} + +bool PythonManager::IsPythonInstalled() { + QString pythonPath = GetPythonPath(); + if (pythonPath.isEmpty()) { + return false; + } + + // Verify Python is executable and correct version + QProcess process; + process.start(pythonPath, QStringList() << "--version"); + if (!process.waitForFinished(3000)) { + return false; + } + + QString output = process.readAllStandardOutput(); + return output.contains("Python 3.9"); +} + +bool PythonManager::ArePackagesInstalled() { + // Fast check: Just verify package directories exist in site-packages + // This is much faster than importing packages (which can take 10+ seconds) + + QString sitePackages = GetSitePackagesPath(); + if (sitePackages.isEmpty()) { + qDebug() << "site-packages directory not found:" << sitePackages; + return false; + } + + // Check if key package directories exist + // Note: BMF is bundled with the app and loaded via PYTHONPATH, not installed here + QStringList requiredPackages = {"torch", "basicsr", "realesrgan"}; + + for (const QString &package : requiredPackages) { + QString packagePath = sitePackages + "/" + package; + if (!QDir(packagePath).exists()) { + qDebug() << "Package directory not found:" << package << "at" << packagePath; + return false; + } + } + + qDebug() << "All required packages found in site-packages"; + return true; +} + +bool PythonManager::CheckSystemPython() { + // Check if system Python 3.9 exists with all required packages + // This is useful in Debug mode to avoid re-downloading Python + // Note: BMF is checked separately since it's bundled with the app + + QStringList pythonCandidates = {"python3.9", "python3"}; + + for (const QString &pythonCmd : pythonCandidates) { + QProcess versionCheck; + versionCheck.start(pythonCmd, QStringList() << "--version"); + if (!versionCheck.waitForFinished(3000)) { + continue; + } + + QString output = versionCheck.readAllStandardOutput(); + if (!output.contains("Python 3.9")) { + continue; + } + + // Found Python 3.9, now check if core AI packages are installed + // BMF is excluded because it's bundled with the app and will be added to PYTHONPATH + QStringList requiredPackages = {"torch", "basicsr", "realesrgan"}; + bool allPackagesFound = true; + + for (const QString &package : requiredPackages) { + QProcess packageCheck; + packageCheck.start(pythonCmd, QStringList() << "-c" << QString("import %1").arg(package)); + if (!packageCheck.waitForFinished(5000) || packageCheck.exitCode() != 0) { + qDebug() << "System Python missing package:" << package; + allPackagesFound = false; + break; + } + } + + if (allPackagesFound) { + qDebug() << "Found system Python 3.9 with required AI packages:" << pythonCmd; + qDebug() << "BMF will be loaded from bundled location"; + return true; + } + } + + return false; +} + +void PythonManager::SetStatus(Status newStatus, const QString &message) { + status = newStatus; + statusMessage = message; + emit StatusChanged(status); + qDebug() << "PythonManager status:" << message; +} + +void PythonManager::SetProgress(int value, const QString &message) { + progress = value; + statusMessage = message; + emit ProgressChanged(progress, statusMessage); +} + +void PythonManager::InstallPython() { + if (status == Status::Installing) { + qWarning() << "Installation already in progress"; + return; + } + + SetStatus(Status::Installing, "Downloading Python 3.9..."); + SetProgress(0, "Starting download..."); + + // Download Python installer + QUrl url(PYTHON_DOWNLOAD_URL); + QNetworkRequest request(url); + + // Follow redirects (GitHub releases redirect to CDN) + request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, + QNetworkRequest::NoLessSafeRedirectPolicy); + + currentDownload = networkManager->get(request); + + connect(currentDownload, &QNetworkReply::downloadProgress, + this, &PythonManager::OnDownloadProgress); + connect(currentDownload, &QNetworkReply::finished, + this, &PythonManager::OnDownloadFinished); +} + +void PythonManager::OnDownloadProgress(qint64 bytesReceived, qint64 bytesTotal) { + if (bytesTotal > 0) { + int percent = (bytesReceived * 50) / bytesTotal; // 0-50% for download + SetProgress(percent, QString("Downloading Python: %1 MB / %2 MB") + .arg(bytesReceived / 1024 / 1024) + .arg(bytesTotal / 1024 / 1024)); + } +} + +void PythonManager::OnDownloadFinished() { + if (!currentDownload) { + return; + } + + if (currentDownload->error() != QNetworkReply::NoError) { + QString error = currentDownload->errorString(); + currentDownload->deleteLater(); + currentDownload = nullptr; + SetStatus(Status::Error, "Download failed: " + error); + emit InstallationFailed(error); + return; + } + + // Save downloaded file + QString tempDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation); + downloadedFilePath = tempDir + "/python-3.9.25.tar.gz"; + + QFile file(downloadedFilePath); + if (!file.open(QIODevice::WriteOnly)) { + QString error = "Failed to save archive: " + file.errorString(); + currentDownload->deleteLater(); + currentDownload = nullptr; + SetStatus(Status::Error, error); + emit InstallationFailed(error); + return; + } + + QByteArray data = currentDownload->readAll(); + if (data.isEmpty()) { + QString error = "Downloaded file is empty (0 bytes). Check network connection."; + file.close(); + currentDownload->deleteLater(); + currentDownload = nullptr; + SetStatus(Status::Error, error); + emit InstallationFailed(error); + return; + } + + file.write(data); + file.close(); + + qDebug() << "Downloaded Python archive:" << downloadedFilePath + << "Size:" << (data.size() / 1024 / 1024) << "MB"; + + currentDownload->deleteLater(); + currentDownload = nullptr; + + SetProgress(50, "Download complete. Extracting Python..."); + + // Extract Python from archive + if (!ExtractPythonArchive(downloadedFilePath)) { + QString error = "Failed to extract Python"; + SetStatus(Status::Error, error); + emit InstallationFailed(error); + return; + } + + SetProgress(100, "Python installation complete"); + SetStatus(Status::Installed, "Python installed successfully"); + emit PythonInstalled(); + + // Clean up downloaded file + QFile::remove(downloadedFilePath); +} + +bool PythonManager::ExtractPythonArchive(const QString &archivePath) { + // Extract .tar.gz archive using tar command + // Python Standalone Builds have structure: python/install/... + QString tempDir = QStandardPaths::writableLocation(QStandardPaths::TempLocation); + QString extractDir = tempDir + "/python_extract"; + + // Clean up any previous extraction + QDir(extractDir).removeRecursively(); + QDir().mkpath(extractDir); + + // Extract tar.gz + QProcess process; + process.start("tar", QStringList() + << "-xzf" + << archivePath + << "-C" + << extractDir); + + if (!process.waitForFinished(120000)) { // 2 minutes timeout + qWarning() << "tar extraction timeout"; + return false; + } + + if (process.exitCode() != 0) { + qWarning() << "tar extraction failed:" << process.readAllStandardError(); + return false; + } + + // Python Standalone Builds extract to: python/ + QString extractedPython = extractDir + "/python"; + if (!QDir(extractedPython).exists()) { + qWarning() << "Extracted Python not found at:" << extractedPython; + return false; + } + + // Move to final location + QString targetPath = GetPythonFrameworkPath(); + QString targetParent = QFileInfo(targetPath).path(); + QDir().mkpath(targetParent); + + // Remove existing Python.framework if it exists + QDir(targetPath).removeRecursively(); + + // Try to rename (fast when same filesystem). If rename fails (different filesystems), + // fall back to recursive copy. + if (QFile::rename(extractedPython, targetPath)) { + QDir(extractDir).removeRecursively(); + qDebug() << "Python extracted successfully to:" << targetPath; + return true; + } + + qWarning() << "Rename failed; attempting recursive copy to:" << targetPath; + // Attempt recursive copy as fallback + if (!CopyDirectoryRecursively(extractedPython, targetPath)) { + qWarning() << "Failed to copy extracted Python to:" << targetPath; + return false; + } + + // Clean up extraction directory + QDir(extractDir).removeRecursively(); + qDebug() << "Python extracted successfully to (copied):" << targetPath; + return true; +} + +void PythonManager::InstallPackages() { + QString pythonPath = GetPythonPath(); + if (pythonPath.isEmpty()) { + QString error = "Python not installed"; + SetStatus(Status::Error, error); + emit InstallationFailed(error); + return; + } + + QString requirementsPath = GetRequirementsPath(); + if (!QFile::exists(requirementsPath)) { + QString error = "requirements.txt not found"; + SetStatus(Status::Error, error); + emit InstallationFailed(error); + return; + } + + SetStatus(Status::Installing, "Installing Python packages..."); + SetProgress(0, "Installing packages from requirements.txt..."); + + // Install packages using pip + installProcess = new QProcess(this); + connect(installProcess, QOverload::of(&QProcess::finished), + this, &PythonManager::OnInstallProcessFinished); + connect(installProcess, &QProcess::readyReadStandardOutput, + this, &PythonManager::OnInstallProcessOutput); + connect(installProcess, &QProcess::readyReadStandardError, + this, &PythonManager::OnInstallProcessOutput); + + // Set working directory to /tmp to avoid macOS permission issues + installProcess->setWorkingDirectory(QStandardPaths::writableLocation(QStandardPaths::TempLocation)); + + // Merge stdout and stderr for better progress tracking + installProcess->setProcessChannelMode(QProcess::MergedChannels); + + installProcess->start(pythonPath, QStringList() + << "-m" << "pip" << "install" +#if defined(__linux__) + // Linux: Use default PyPI indexes avoid installing CUDA packages + << "--index-url" << "https://download.pytorch.org/whl/cpu" + << "--extra-index-url" << "https://pypi.org/simple" +#endif + << "-r" << requirementsPath +#if defined(__linux__) + // Linux: Install BMF from GitHub release +#if defined(__aarch64__) + << "https://github.com/OpenConverterLab/bmf/releases/download/oc0.0.5/BabitMF-0.2.0-cp39-cp39-manylinux_2_28_aarch64.whl" +#else + << "https://github.com/OpenConverterLab/bmf/releases/download/oc0.0.5/BabitMF-0.2.0-cp39-cp39-manylinux_2_28_x86_64.whl" +#endif +#endif + << "--no-cache-dir" + << "--progress-bar" << "on"); +} + +void PythonManager::OnInstallProcessOutput() { + if (!installProcess) return; + + QString output = installProcess->readAllStandardOutput(); + QStringList lines = output.split('\n', Qt::SkipEmptyParts); + + for (const QString &line : lines) { + // Parse pip progress: "Downloading package-name (X.X MB)" + // or "Installing collected packages: ..." + if (line.contains("Downloading") || line.contains("Installing")) { + // Simple progress estimation based on output + static int packageCount = 0; + packageCount++; + int progress = qMin(90, packageCount * 10); // Cap at 90% until finished + SetProgress(progress, line.trimmed()); + } + } +} + +void PythonManager::OnInstallProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) { + if (exitStatus != QProcess::NormalExit || exitCode != 0) { + QString error = "Package installation failed: " + installProcess->readAll(); + SetStatus(Status::Error, error); + emit InstallationFailed(error); + installProcess->deleteLater(); + installProcess = nullptr; + return; + } + + // Pip install succeeded + // Note: BMF Python bindings are loaded via PYTHONPATH in transcoder_bmf.cpp + // from app bundle's Resources/bmf directory, so no need to copy them here + SetProgress(100, "All packages installed successfully"); + SetStatus(Status::Installed, "Python and packages ready"); + emit PackagesInstalled(); + + installProcess->deleteLater(); + installProcess = nullptr; +} + +void PythonManager::CancelInstallation() { + if (currentDownload) { + currentDownload->abort(); + } + if (installProcess) { + installProcess->kill(); + } + SetStatus(Status::NotInstalled, "Installation cancelled"); +} + +bool PythonManager::CopyDirectoryRecursively(const QString &source, const QString &destination) { + QDir sourceDir(source); + if (!sourceDir.exists()) { + return false; + } + + QDir destDir(destination); + if (!destDir.exists()) { + destDir.mkpath("."); + } + + // Copy all files + QFileInfoList entries = sourceDir.entryInfoList(QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot); + for (const QFileInfo &entry : entries) { + QString srcPath = entry.absoluteFilePath(); + QString dstPath = destination + "/" + entry.fileName(); + + if (entry.isDir()) { + // Recursively copy subdirectory + if (!CopyDirectoryRecursively(srcPath, dstPath)) { + return false; + } + } else { + // Copy file + if (!QFile::copy(srcPath, dstPath)) { + qWarning() << "Failed to copy file:" << srcPath << "to" << dstPath; + return false; + } + } + } + + return true; +} diff --git a/src/builder/src/transcode_page.cpp b/src/builder/src/transcode_page.cpp index bc80d82f..6fb5ec23 100644 --- a/src/builder/src/transcode_page.cpp +++ b/src/builder/src/transcode_page.cpp @@ -72,7 +72,7 @@ void TranscodePage::SetupUI() { tr("Input File"), FileSelectorWidget::InputFile, tr("Select a media file or click Batch for multiple files..."), - tr("Media Files (*.mp4 *.avi *.mkv *.mov *.flv *.wmv *.webm *.ts *.m4v);;All Files (*.*)"), + tr("All Files (*.*)"), tr("Select Media File"), this ); @@ -159,7 +159,7 @@ void TranscodePage::SetupUI() { formatLabel = new QLabel(tr("Format:"), formatGroupBox); formatComboBox = new QComboBox(formatGroupBox); - formatComboBox->addItems({"mp4", "mkv", "avi", "mov", "flv", "webm", "ts"}); + formatComboBox->addItems({"mp4", "mkv", "avi", "mov", "flv", "webm", "ts", "jpg", "png"}); formatComboBox->setCurrentText("mp4"); connect(formatComboBox, QOverload::of(&QComboBox::currentIndexChanged), this, &TranscodePage::OnFormatChanged); diff --git a/src/common/include/encode_parameter.h b/src/common/include/encode_parameter.h index a2264f51..4d01879c 100644 --- a/src/common/include/encode_parameter.h +++ b/src/common/include/encode_parameter.h @@ -21,6 +21,11 @@ #include #include +enum class AlgoMode { + None, + Upscale, +}; + class EncodeParameter { private: bool available; @@ -42,6 +47,10 @@ class EncodeParameter { double startTime; // in seconds double endTime; // in seconds + AlgoMode algoMode; + + int upscaleFactor; + public: EncodeParameter(); ~EncodeParameter(); @@ -91,6 +100,14 @@ class EncodeParameter { double get_start_time(); double get_end_time(); + + AlgoMode get_algo_mode(); + + void set_algo_mode(AlgoMode am); + + int get_upscale_factor(); + + void set_upscale_factor(int uf); }; #endif // ENCODEPARAMETER_H diff --git a/src/common/src/encode_parameter.cpp b/src/common/src/encode_parameter.cpp index a5bb8854..a98c7b25 100644 --- a/src/common/src/encode_parameter.cpp +++ b/src/common/src/encode_parameter.cpp @@ -34,6 +34,9 @@ EncodeParameter::EncodeParameter() { startTime = -1.0; endTime = -1.0; + algoMode = AlgoMode::None; + upscaleFactor = 2; + available = false; } @@ -131,4 +134,18 @@ double EncodeParameter::get_start_time() { return startTime; } double EncodeParameter::get_end_time() { return endTime; } +void EncodeParameter::set_algo_mode(AlgoMode am) { + algoMode = am; + available = true; +} + +AlgoMode EncodeParameter::get_algo_mode() { return algoMode; } + +void EncodeParameter::set_upscale_factor(int uf) { + upscaleFactor = uf; + available = true; +} + +int EncodeParameter::get_upscale_factor() { return upscaleFactor; } + EncodeParameter::~EncodeParameter() {} diff --git a/src/component/include/python_install_dialog.h b/src/component/include/python_install_dialog.h new file mode 100644 index 00000000..ebb9a115 --- /dev/null +++ b/src/component/include/python_install_dialog.h @@ -0,0 +1,72 @@ +/* + * Copyright 2025 Jack Lau + * Email: jacklau1222gm@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#ifndef PYTHON_INSTALL_DIALOG_H +#define PYTHON_INSTALL_DIALOG_H + +#include +#include +#include +#include +#include +#include "python_manager.h" + +/** + * @brief Dialog for installing Python runtime and packages + * + * Shows when user tries to use AI Processing but Python is not installed. + * Provides user-friendly interface for downloading and installing Python. + */ +class PythonInstallDialog : public QDialog { + Q_OBJECT + +public: + explicit PythonInstallDialog(QWidget *parent = nullptr); + ~PythonInstallDialog(); + + /** + * @brief Check if installation was successful + */ + bool WasSuccessful() const { return installSuccess; } + +private slots: + void OnInstallClicked(); + void OnCancelClicked(); + void OnStatusChanged(PythonManager::Status status); + void OnProgressChanged(int progress, const QString &message); + void OnPythonInstalled(); + void OnPackagesInstalled(); + void OnInstallationFailed(const QString &error); + +private: + void SetupUI(); + void RetranslateUi(); + + PythonManager *pythonManager; + + QLabel *titleLabel; + QLabel *descriptionLabel; + QLabel *statusLabel; + QProgressBar *progressBar; + QPushButton *installButton; + QPushButton *cancelButton; + + bool installSuccess; + bool installingPython; // true = installing Python, false = installing packages +}; + +#endif // PYTHON_INSTALL_DIALOG_H diff --git a/src/component/src/python_install_dialog.cpp b/src/component/src/python_install_dialog.cpp new file mode 100644 index 00000000..736bee6e --- /dev/null +++ b/src/component/src/python_install_dialog.cpp @@ -0,0 +1,231 @@ +/* + * Copyright 2025 Jack Lau + * Email: jacklau1222gm@gmail.com + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "python_install_dialog.h" +#include +#include +#include + +PythonInstallDialog::PythonInstallDialog(QWidget *parent) + : QDialog(parent) + , pythonManager(new PythonManager(this)) + , installSuccess(false) + , installingPython(true) +{ + SetupUI(); + RetranslateUi(); + + // Connect signals + connect(pythonManager, &PythonManager::StatusChanged, + this, &PythonInstallDialog::OnStatusChanged); + connect(pythonManager, &PythonManager::ProgressChanged, + this, &PythonInstallDialog::OnProgressChanged); + connect(pythonManager, &PythonManager::PythonInstalled, + this, &PythonInstallDialog::OnPythonInstalled); + connect(pythonManager, &PythonManager::PackagesInstalled, + this, &PythonInstallDialog::OnPackagesInstalled); + connect(pythonManager, &PythonManager::InstallationFailed, + this, &PythonInstallDialog::OnInstallationFailed); + + connect(installButton, &QPushButton::clicked, + this, &PythonInstallDialog::OnInstallClicked); + connect(cancelButton, &QPushButton::clicked, + this, &PythonInstallDialog::OnCancelClicked); +} + +PythonInstallDialog::~PythonInstallDialog() { +} + +void PythonInstallDialog::SetupUI() { + setWindowTitle("Install Python Runtime"); + setMinimumWidth(500); + setModal(true); + + QVBoxLayout *mainLayout = new QVBoxLayout(this); + mainLayout->setSpacing(15); + mainLayout->setContentsMargins(20, 20, 20, 20); + + // Title + titleLabel = new QLabel(this); + QFont titleFont = titleLabel->font(); + titleFont.setPointSize(16); + titleFont.setBold(true); + titleLabel->setFont(titleFont); + mainLayout->addWidget(titleLabel); + + // Description + descriptionLabel = new QLabel(this); + descriptionLabel->setWordWrap(true); + mainLayout->addWidget(descriptionLabel); + + // Status label + statusLabel = new QLabel(this); + statusLabel->setWordWrap(true); + mainLayout->addWidget(statusLabel); + + // Progress bar + progressBar = new QProgressBar(this); + progressBar->setRange(0, 100); + progressBar->setValue(0); + progressBar->setVisible(false); + mainLayout->addWidget(progressBar); + + mainLayout->addStretch(); + + // Buttons + QHBoxLayout *buttonLayout = new QHBoxLayout(); + buttonLayout->addStretch(); + + installButton = new QPushButton(this); + installButton->setDefault(true); + buttonLayout->addWidget(installButton); + + cancelButton = new QPushButton(this); + buttonLayout->addWidget(cancelButton); + + mainLayout->addLayout(buttonLayout); +} + +void PythonInstallDialog::RetranslateUi() { + titleLabel->setText(tr("AI Processing Setup")); + + descriptionLabel->setText(tr( + "AI Processing requires Python 3.9 and additional packages (PyTorch, BasicSR, Real-ESRGAN).\n\n" + "This will download and install:\n" + "• Python 3.9 runtime (~18 MB)\n" + "• AI processing packages (~500 MB)\n\n" + "The installation is completely isolated and will not affect your system Python." + )); + + statusLabel->setText(tr("Ready to install")); + installButton->setText(tr("Install")); + cancelButton->setText(tr("Cancel")); +} + +void PythonInstallDialog::OnInstallClicked() { + installButton->setEnabled(false); + progressBar->setVisible(true); + + if (installingPython) { + pythonManager->InstallPython(); + } else { + pythonManager->InstallPackages(); + } +} + +void PythonInstallDialog::OnCancelClicked() { + if (pythonManager->GetStatus() == PythonManager::Status::Installing) { + QMessageBox::StandardButton reply = QMessageBox::question( + this, + tr("Cancel Installation"), + tr("Are you sure you want to cancel the installation?"), + QMessageBox::Yes | QMessageBox::No + ); + + if (reply == QMessageBox::Yes) { + pythonManager->CancelInstallation(); + reject(); + } + } else { + reject(); + } +} + +void PythonInstallDialog::OnStatusChanged(PythonManager::Status status) { + switch (status) { + case PythonManager::Status::NotInstalled: + statusLabel->setText(tr("Not installed")); + installButton->setEnabled(true); + break; + + case PythonManager::Status::Installing: + statusLabel->setText(tr("Installing...")); + installButton->setEnabled(false); + cancelButton->setText(tr("Cancel")); + break; + + case PythonManager::Status::Installed: + statusLabel->setText(tr("Installation complete!")); + installButton->setEnabled(false); + cancelButton->setText(tr("Close")); + break; + + case PythonManager::Status::Error: + statusLabel->setText(tr("Installation failed")); + installButton->setEnabled(true); + installButton->setText(tr("Retry")); + break; + } +} + +void PythonInstallDialog::OnProgressChanged(int progress, const QString &message) { + progressBar->setValue(progress); + statusLabel->setText(message); +} + +void PythonInstallDialog::OnPythonInstalled() { + // Python installed, now install packages + installingPython = false; + statusLabel->setText(tr("Python installed. Installing packages...")); + pythonManager->InstallPackages(); +} + +void PythonInstallDialog::OnPackagesInstalled() { + installSuccess = true; + progressBar->setValue(100); + statusLabel->setText(tr("Installation complete! AI Processing is now ready.")); + + QMessageBox msgBox(this); + msgBox.setIcon(QMessageBox::Information); + msgBox.setWindowTitle(tr("Installation Complete")); + msgBox.setText(tr( + "Python and all required packages have been installed successfully.\n\n" + "A restart is required to enable AI Processing features." + )); + + QPushButton *restartButton = + msgBox.addButton(tr("Restart Now"), QMessageBox::AcceptRole); + QPushButton *laterButton = + msgBox.addButton(tr("Restart Later"), QMessageBox::RejectRole); + + msgBox.exec(); + + if (msgBox.clickedButton() == restartButton) { + // Relaunch the application + const QString program = QCoreApplication::applicationFilePath(); + const QStringList arguments = QCoreApplication::arguments(); + + QProcess::startDetached(program, arguments); + QCoreApplication::quit(); + return; + } + + accept(); +} + +void PythonInstallDialog::OnInstallationFailed(const QString &error) { + progressBar->setVisible(false); + installButton->setEnabled(true); + installButton->setText(tr("Retry")); + + QMessageBox::critical( + this, + tr("Installation Failed"), + tr("Failed to install Python runtime:\n\n%1\n\n" + "Please check your internet connection and try again.").arg(error) + ); +} diff --git a/src/engine/src/converter.cpp b/src/engine/src/converter.cpp index 4e54fa21..79e2b25e 100644 --- a/src/engine/src/converter.cpp +++ b/src/engine/src/converter.cpp @@ -89,18 +89,6 @@ bool Converter::set_transcoder(std::string transcoderName) { } bool Converter::convert_format(const std::string &src, const std::string &dst) { - if (encodeParameter->get_video_codec_name() == "") { - copyVideo = true; - } else { - copyVideo = false; - } - - if (encodeParameter->get_audio_codec_name() == "") { - copyAudio = true; - } else { - copyAudio = false; - } - return transcoder->transcode(src, dst); } diff --git a/src/main.cpp b/src/main.cpp index 5064d61d..c224d76f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -20,7 +20,15 @@ static bool is_existing_regular_file(const fs::path &p) { static bool is_valid_output_candidate(const fs::path &p) { if (!p.has_filename()) return false; // reject directory-only paths fs::path parent = p.parent_path(); - if (parent.empty()) parent = fs::current_path(); + if (parent.empty()) { + try { + parent = fs::current_path(); + } catch (const fs::filesystem_error& e) { + // If we can't get current directory, assume current directory is valid + std::cerr << "Warning: Failed to get current directory: " << e.what() << std::endl; + return true; // Assume valid if we can't check + } + } if (fs::exists(p)) return !fs::is_directory(p); // existing file ok (not a dir) return fs::exists(parent) && fs::is_directory(parent); // non-existing file OK if parent dir exists } @@ -38,6 +46,7 @@ void printUsage(const char *programName) { << " -b:a, --bitrate:audio BITRATE Set bitrate for audio codec\n" << " -pix_fmt PIX_FMT Set pixel format for video\n" << " -scale SCALE(w)x(h) Set scale for video (width x height)\n" + << " -upscale FACTOR Enable AI upscaling with factor (e.g., 2, 4) [requires BMF]\n" << " -ss START_TIME Set start time for cutting (format: HH:MM:SS or seconds)\n" << " -to END_TIME Set end time for cutting (format: HH:MM:SS or seconds)\n" << " -t DURATION Set duration for cutting (format: HH:MM:SS or seconds)\n" @@ -164,6 +173,7 @@ bool handleCLI(int argc, char *argv[]) { double startTime = -1.0; double endTime = -1.0; double duration = -1.0; + int upscaleFactor = -1; // Parse command line arguments for (int i = 1; i < argc; i++) { @@ -242,6 +252,14 @@ bool handleCLI(int argc, char *argv[]) { return false; } } + } else if (strcmp(argv[i], "-upscale") == 0) { + if (i + 1 < argc) { + upscaleFactor = std::stoi(argv[++i]); + if (upscaleFactor <= 0) { + std::cerr << "Error: Upscale factor must be positive\n"; + return false; + } + } } else { // positional argument: validate as input (existing) or output (candidate) fs::path p(argv[i]); @@ -300,6 +318,19 @@ bool handleCLI(int argc, char *argv[]) { encodeParam->set_audio_bit_rate(audioBitRate); } + // Handle upscale parameters + if (upscaleFactor > 0) { + encodeParam->set_algo_mode(AlgoMode::Upscale); + encodeParam->set_upscale_factor(upscaleFactor); + std::cout << "AI upscaling enabled with factor: " << upscaleFactor << "\n"; + + // Upscaling requires BMF transcoder + if (transcoderType != "BMF") { + std::cout << "Note: Upscaling requires BMF transcoder. Switching to BMF.\n"; + transcoderType = "BMF"; + } + } + // Handle time parameters with validation if (startTime >= 0.0) { encodeParam->set_start_time(startTime); diff --git a/src/modules/enhance_module.py b/src/modules/enhance_module.py new file mode 100644 index 00000000..7950030d --- /dev/null +++ b/src/modules/enhance_module.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +# This code is based on the implementation from https://github.com/xinntao/Real-ESRGAN + +from basicsr.archs.rrdbnet_arch import RRDBNet +from basicsr.utils.download_util import load_file_from_url + +from realesrgan import RealESRGANer +from realesrgan.archs.srvgg_arch import SRVGGNetCompact + +from bmf import Module, Log, Timestamp, ProcessResult, LogLevel, Packet, VideoFrame +from bmf.lib._bmf.sdk import ffmpeg + +from bmf import hmp as mp +import numpy as np + +import os + +import torch + +import platform + +def load_model(): + model = SRVGGNetCompact( + num_in_ch=3, + num_out_ch=3, + num_feat=64, + num_conv=16, + upscale=4, + act_type="prelu", + ) + netscale = 4 + file_url = [ + "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.5.0/realesr-animevideov3.pth" + ] + return model, netscale, file_url + + +def prepare_model(model_name, file_url): + # Get the directory where this module is located + ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) + model_path = os.path.join(ROOT_DIR, "weights", model_name + ".pth") + + # Check if model already exists (bundled or previously downloaded) + if not os.path.isfile(model_path): + print(f"Model not found at {model_path}, attempting to download...") + for url in file_url: + # model_path will be updated + model_path = load_file_from_url( + url=url, + model_dir=os.path.join(ROOT_DIR, "weights"), + progress=True, + file_name=None, + ) + else: + print(f"Using bundled model from: {model_path}") + + return model_path + + +class EnhanceModule(Module): + + def __init__(self, node=None, option=None): + self._node = node + if not option: + Log.log_node(LogLevel.ERROR, self._node, "no option") + return + + self.frame_number = 0 + + tile = option.get("tile", 0) + tile_pad = option.get("tile_pad", 10) + pre_pad = option.get("pre_pad", 10) + fp32 = option.get("fp32", False) + gpu_id = option.get("gpu_id", 0) + + self.output_scale = option.get("output_scale", None) + + # Agregar estas líneas para verificar el dispositivo + print("Checking available device...") + if platform.system() == "Darwin" and hasattr(torch.backends, "mps") and torch.backends.mps.is_available(): + print("MPS is available - using M1 GPU") + device = torch.device("mps") + else: + print("MPS is not available - auto select by Real-ESRGAN") + device = None + + model, netscale, file_url = load_model() + model_name = "realesr-animevideov3" # x4 VGG-style model (XS size) + model_path = prepare_model(model_name, file_url) + + self.upsampler = RealESRGANer( + scale=netscale, + model_path=model_path, + dni_weight=None, + model=model, + tile=tile, + tile_pad=tile_pad, + pre_pad=pre_pad, + half=not fp32, + gpu_id=gpu_id, + device=device, + ) + + def process(self, task): + output_queue = task.get_outputs().get(0, None) + input_queue = task.get_inputs().get(0, None) + + while not input_queue.empty(): + pkt = input_queue.get() + # process EOS + if pkt.timestamp == Timestamp.EOF: + Log.log_node(LogLevel.INFO, task.get_node(), "Receive EOF") + if output_queue is not None: + output_queue.put(Packet.generate_eof_packet()) + task.timestamp = Timestamp.DONE + return ProcessResult.OK + + video_frame = pkt.get(VideoFrame) + # use ffmpeg + frame = ffmpeg.reformat(video_frame, + "rgb24").frame().plane(0).numpy() + + output, _ = self.upsampler.enhance(frame, self.output_scale) + Log.log_node( + LogLevel.INFO, + self._node, + "enhance output shape: ", + output.shape, + " flags: ", + output.flags, + ) + output = np.ascontiguousarray(output) + rgbformat = mp.PixelInfo(mp.kPF_RGB24) + image = mp.Frame(mp.from_numpy(output), rgbformat) + + output_frame = VideoFrame(image) + Log.log_node(LogLevel.INFO, self._node, "output video frame") + + output_frame.pts = video_frame.pts + output_frame.time_base = video_frame.time_base + output_pkt = Packet(output_frame) + output_pkt.timestamp = pkt.timestamp + if output_queue is not None: + output_queue.put(output_pkt) + if self.callback_ is not None: + self.frame_number += 1 + message = "frame number: " + str(self.frame_number) + self.callback_(0, bytes(message, "utf-8")) + + return ProcessResult.OK diff --git a/src/resources/lang_chinese.qm b/src/resources/lang_chinese.qm index 2ec24884..74f6bee9 100644 Binary files a/src/resources/lang_chinese.qm and b/src/resources/lang_chinese.qm differ diff --git a/src/resources/lang_chinese.ts b/src/resources/lang_chinese.ts index f3199c29..caeda3ce 100644 --- a/src/resources/lang_chinese.ts +++ b/src/resources/lang_chinese.ts @@ -1226,6 +1226,34 @@ FFTOOL FFTOOL + + AI Processing + AI 处理 + + + Select Python site-packages Directory + 选择 Python site-packages 目录 + + + Python path set to: %1 + Python 路径设置为:%1 + + + Using App Python + 使用应用内置 Python + + + App Python + 应用内置 Python + + + Custom Path... + 自定义路径... + + + Python + Python + QObject @@ -1569,4 +1597,145 @@ 转码 + + AIProcessingPage + + Python Required + 需要 Python + + + AI Processing requires Python 3.9 and additional packages. + +Would you like to download and install them now? +(Download size: ~550 MB, completely isolated from system Python) + AI 处理需要 Python 3.9 及其他依赖包。 + +是否现在下载并安装? +(下载大小:约 550 MB,与系统 Python 完全隔离) + + + AI Processing Unavailable + AI 处理不可用 + + + AI Processing features require Python to be installed. + +You can install it later by returning to this page. + AI 处理功能需要安装 Python。 + +您可以稍后返回此页面进行安装。 + + + Input File + 输入文件 + + + Select a media file or click Batch for multiple files... + 选择媒体文件或点击批量处理多个文件... + + + All Files (*.*) + 所有文件 (*.*) + + + Select Media File + 选择媒体文件 + + + Algorithm + 算法 + + + Select Algorithm: + 选择算法: + + + Upscaler + 超分辨率 + + + Algorithm Settings + 算法设置 + + + Upscale Factor: + 放大倍数: + + + Video Settings + 视频设置 + + + Audio Settings + 音频设置 + + + Codec: + 编解码器: + + + Bitrate: + 比特率: + + + File Format + 文件格式 + + + Format: + 格式: + + + Output File + 输出文件 + + + Output file path... + 输出文件路径... + + + Process / Add to Queue + 处理 / 添加到队列 + + + Add to Queue + 添加到队列 + + + Process + 处理 + + + Processing... + 处理中... + + + Success + 成功 + + + AI processing completed successfully! + AI 处理成功完成! + + + Error + 错误 + + + Failed to process file. + 文件处理失败。 + + + Warning + 警告 + + + Please select an input file. + 请选择输入文件。 + + + Please select an output file. + 请选择输出文件。 + + diff --git a/src/resources/linglong.yaml b/src/resources/linglong.yaml new file mode 100644 index 00000000..dc55d498 --- /dev/null +++ b/src/resources/linglong.yaml @@ -0,0 +1,31 @@ +version: "1" + +package: + id: io.github.openconverterlab + name: "OpenConverter" + version: 1.5.2.0 + kind: app + description: | + OpenConverter + +base: org.deepin.base/23.1.0 + +command: + - /opt/apps/io.github.openconverterlab/files/bin/run.sh + +source: + - kind: local + name: "OpenConveter" + +build: | + mkdir -p ${PREFIX}/bin ${PREFIX}/lib ${PREFIX}/share + rm -rf binary/bmf/bin + cp -rf binary/* ${PREFIX}/bin + cp -rf binary/lib/* ${PREFIX}/lib + cp -rf template_app/* ${PREFIX}/share + echo "#!/usr/bin/env bash" > $PREFIX/bin/run.sh + echo "export LD_LIBRARY_PATH=~/.local/share/OpenConverter/Python.framework/lib" >> ${PREFIX}/bin/run.sh + echo "export BMF_MODULE_PATH=${PREFIX}/bin/modules" >> ${PREFIX}/bin/run.sh + echo "export PYTHONPATH=${PREFIX}/bin/bmf:${PREFIX}/bin/bmf/lib" >> ${PREFIX}/bin/run.sh + echo "${PREFIX}/bin/OpenConverter" >> ${PREFIX}/bin/run.sh + chmod +x ${PREFIX}/bin/run.sh diff --git a/src/resources/requirements.txt b/src/resources/requirements.txt new file mode 100644 index 00000000..411fbd6b --- /dev/null +++ b/src/resources/requirements.txt @@ -0,0 +1,12 @@ +# OpenConverter AI Processing Requirements +# Python 3.9 required + +# Core deep learning framework +torchvision<=0.12.0 + +# Image super-resolution +basicsr==1.4.2 +realesrgan==0.3.0 + +# Scientific computing +numpy<2 diff --git a/src/transcoder/include/transcoder_bmf.h b/src/transcoder/include/transcoder_bmf.h index ddc9ced5..73d38b9f 100644 --- a/src/transcoder/include/transcoder_bmf.h +++ b/src/transcoder/include/transcoder_bmf.h @@ -46,6 +46,13 @@ class TranscoderBMF : public Transcoder { nlohmann::json decoder_para; nlohmann::json encoder_para; + + // Helper function to set up Python environment (PYTHONPATH) + // Returns true if setup succeeded, false if App Python is not installed + bool setup_python_environment(); + + // Helper function to get the Python module path + std::string get_python_module_path(); }; #endif // TRANSCODER_BMF_H diff --git a/src/transcoder/src/transcoder_bmf.cpp b/src/transcoder/src/transcoder_bmf.cpp index fa9d8da8..de0e01d6 100644 --- a/src/transcoder/src/transcoder_bmf.cpp +++ b/src/transcoder/src/transcoder_bmf.cpp @@ -14,6 +14,20 @@ */ #include "../include/transcoder_bmf.h" +#include +#include + +#ifdef ENABLE_GUI +#include +#include +#include +#include +#endif + +#ifdef __APPLE__ +#include +#include +#endif /* Receive pointers from converter */ TranscoderBMF::TranscoderBMF(ProcessParameter *process_parameter, @@ -22,6 +36,345 @@ TranscoderBMF::TranscoderBMF(ProcessParameter *process_parameter, frame_total_number = 0; } +bool TranscoderBMF::setup_python_environment() { + // In Debug mode, use system PYTHONPATH from environment (set by developer/CMake) + // In Release mode, set up PYTHONPATH for bundled BMF and external Python (App Support or Custom) +#ifndef NDEBUG + // Debug mode: Set PYTHONPATH based on BMF_ROOT_PATH from environment or CMake + BMFLOG(BMF_INFO) << "Debug mode: Setting PYTHONPATH from BMF_ROOT_PATH"; + + // Get BMF_ROOT_PATH from environment or CMake + const char* bmf_root_env = std::getenv("BMF_ROOT_PATH"); + std::string bmf_root; + + if (bmf_root_env) { + bmf_root = std::string(bmf_root_env); + BMFLOG(BMF_INFO) << "Using BMF_ROOT_PATH from environment: " << bmf_root; + } +#ifdef BMF_ROOT_PATH_STR + else { + bmf_root = BMF_ROOT_PATH_STR; + BMFLOG(BMF_INFO) << "Using BMF_ROOT_PATH from CMake: " << bmf_root; + } +#endif + + if (!bmf_root.empty()) { + // Normalize BMF_ROOT_PATH to include /output/bmf if needed + if (bmf_root.find("output/bmf") == std::string::npos) { + bmf_root += "/output/bmf"; + } + + // Set PYTHONPATH: BMF_ROOT_PATH/lib:BMF_ROOT_PATH (parent of /output/bmf) + std::string bmf_lib_path = bmf_root + "/lib"; + size_t output_pos = bmf_root.find("/output/bmf"); + std::string bmf_output_path; + if (output_pos != std::string::npos) { + bmf_output_path = bmf_root.substr(0, output_pos) + "/output"; + } else { + bmf_output_path = bmf_root; + } + + // Get existing PYTHONPATH + std::string current_pythonpath; + const char* existing_pythonpath = std::getenv("PYTHONPATH"); + if (existing_pythonpath) { + current_pythonpath = existing_pythonpath; + } + + // Set PYTHONPATH: bmf/lib:bmf/output:existing + std::string new_pythonpath = bmf_lib_path + ":" + bmf_output_path; + if (!current_pythonpath.empty()) { + new_pythonpath += ":" + current_pythonpath; + } + + setenv("PYTHONPATH", new_pythonpath.c_str(), 1); + BMFLOG(BMF_INFO) << "Set PYTHONPATH: " << new_pythonpath; + + // Set BMF_MODULE_CONFIG_PATH + setenv("BMF_MODULE_CONFIG_PATH", bmf_root.c_str(), 1); + BMFLOG(BMF_INFO) << "Set BMF_MODULE_CONFIG_PATH: " << bmf_root; + } else { + BMFLOG(BMF_WARNING) << "BMF_ROOT_PATH not set. Please set it in environment or CMake."; + BMFLOG(BMF_WARNING) << "Example: export BMF_ROOT_PATH=/path/to/bmf"; + } +#ifndef __linux__ + return true; // Debug mode always succeeds (uses system Python) +#endif +#endif + + // Release mode: Set up PYTHONPATH for bundled BMF libraries and external Python + std::string bmf_lib_path; + std::string bmf_output_path; + std::string bmf_config_path; + std::string python_site_packages; + +#ifdef ENABLE_GUI + // Get Python path from QSettings (default to App Python in Application Support) + QSettings settings("OpenConverter", "OpenConverter"); + QString pythonMode = settings.value("python/mode", "pythonAppSupport").toString(); + std::string python_bin_path; + std::string python_lib_path; + + if (pythonMode == "pythonCustom") { + QString customPath = settings.value("python/customPath", "").toString(); + if (!customPath.isEmpty()) { + python_site_packages = customPath.toStdString(); + BMFLOG(BMF_INFO) << "Using custom Python site-packages: " << python_site_packages; + + // Derive bin and lib paths from custom site-packages path + // site-packages is typically at: /path/to/python/lib/python3.x/site-packages + // We need: bin at /path/to/python/bin, lib at /path/to/python/lib + std::filesystem::path site_pkg_path(python_site_packages); + // Go up: site-packages -> python3.x -> lib -> python_root + std::filesystem::path python_root = site_pkg_path.parent_path().parent_path().parent_path(); + python_bin_path = (python_root / "bin").string(); + python_lib_path = (python_root / "lib").string(); + BMFLOG(BMF_INFO) << "Custom Python bin path: " << python_bin_path; + BMFLOG(BMF_INFO) << "Custom Python lib path: " << python_lib_path; + } + } + + // Default to App Python if not custom + if (python_site_packages.empty()) { + // Use QStandardPaths for cross-platform app data location + // macOS: ~/Library/Application Support/OpenConverter + // Linux: ~/.local/share/OpenConverter + // Windows: C:/Users//AppData/Local/OpenConverter + QString appSupportDir = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation); + QString pythonFramework = appSupportDir + "/Python.framework"; + QString sitePackages = pythonFramework + "/lib/python3.9/site-packages"; + + // Check if App Python exists - NO FALLBACK, App Python is required + if (QDir(sitePackages).exists()) { + python_site_packages = sitePackages.toStdString(); + python_bin_path = (pythonFramework + "/bin").toStdString(); + python_lib_path = (pythonFramework + "/lib").toStdString(); + BMFLOG(BMF_INFO) << "Using App Python site-packages: " << python_site_packages; + BMFLOG(BMF_INFO) << "App Python bin path: " << python_bin_path; + BMFLOG(BMF_INFO) << "App Python lib path: " << python_lib_path; + } else { + BMFLOG(BMF_ERROR) << "App Python not found at: " << sitePackages.toStdString(); + BMFLOG(BMF_ERROR) << "Please install App Python via Python menu -> Install Python"; + return false; // Don't continue without App Python - no fallback to system Python + } + } + + // Set PATH and LD_LIBRARY_PATH/DYLD_LIBRARY_PATH for Python + if (!python_bin_path.empty()) { + // Prepend Python bin to PATH + std::string current_path; + const char* existing_path = std::getenv("PATH"); + if (existing_path) { + current_path = existing_path; + } + std::string new_path = python_bin_path; + if (!current_path.empty()) { + new_path += ":" + current_path; + } + setenv("PATH", new_path.c_str(), 1); + BMFLOG(BMF_INFO) << "Set PATH: " << new_path; + } + + if (!python_lib_path.empty()) { +#ifdef __APPLE__ + // On macOS, set DYLD_LIBRARY_PATH + std::string current_dyld; + const char* existing_dyld = std::getenv("DYLD_LIBRARY_PATH"); + if (existing_dyld) { + current_dyld = existing_dyld; + } + std::string new_dyld = python_lib_path; + if (!current_dyld.empty()) { + new_dyld += ":" + current_dyld; + } + setenv("DYLD_LIBRARY_PATH", new_dyld.c_str(), 1); + BMFLOG(BMF_INFO) << "Set DYLD_LIBRARY_PATH: " << new_dyld; +#else + // On Linux, set LD_LIBRARY_PATH + std::string current_ld; + const char* existing_ld = std::getenv("LD_LIBRARY_PATH"); + if (existing_ld) { + current_ld = existing_ld; + } + std::string new_ld = python_lib_path; + if (!current_ld.empty()) { + new_ld += ":" + current_ld; + } + setenv("LD_LIBRARY_PATH", new_ld.c_str(), 1); + BMFLOG(BMF_INFO) << "Set LD_LIBRARY_PATH: " << new_ld; +#endif + } +#endif + +#ifdef __APPLE__ + // Check if running from app bundle + char exe_path[1024]; + uint32_t size = sizeof(exe_path); + if (_NSGetExecutablePath(exe_path, &size) == 0) { + std::string exe_dir = std::string(exe_path); + size_t last_slash = exe_dir.find_last_of('/'); + if (last_slash != std::string::npos) { + exe_dir = exe_dir.substr(0, last_slash); + + // Check if we're in an app bundle (path contains .app/Contents/MacOS) + if (exe_dir.find(".app/Contents/MacOS") != std::string::npos) { + size_t app_pos = exe_dir.find(".app/Contents/MacOS"); + std::string app_bundle = exe_dir.substr(0, app_pos + 4); // Include .app + + // Check if BMF libraries are bundled (Release build) + std::string bundled_bmf_lib = app_bundle + "/Contents/Frameworks/lib"; + std::string bundled_config = app_bundle + "/Contents/Frameworks/BUILTIN_CONFIG.json"; + std::ifstream bmf_check(bundled_config); + + if (bmf_check.good()) { + // BMF libraries are bundled (Release build) + bmf_lib_path = bundled_bmf_lib; + bmf_output_path = app_bundle + "/Contents/Frameworks"; + bmf_config_path = app_bundle + "/Contents/Frameworks"; + BMFLOG(BMF_INFO) << "Using bundled BMF libraries from: " << bmf_lib_path; + } else { + // App bundle exists but BMF not bundled (should not happen in Release) + BMFLOG(BMF_WARNING) << "App bundle detected but BMF not bundled"; + } + bmf_check.close(); + + // Check for bundled BMF Python package in Resources/bmf/ + // The bmf package is at Resources/bmf/, so we add Resources/ to PYTHONPATH + std::string bundled_bmf_python = app_bundle + "/Contents/Resources/bmf"; + std::string resources_dir = app_bundle + "/Contents/Resources"; + std::ifstream bmf_python_check(bundled_bmf_python + "/__init__.py"); + if (bmf_python_check.good()) { + // Add Resources directory to bmf_output_path so 'import bmf' finds Resources/bmf/ + if (!bmf_output_path.empty()) { + bmf_output_path = resources_dir + ":" + bmf_output_path; + } else { + bmf_output_path = resources_dir; + } + BMFLOG(BMF_INFO) << "Found bundled BMF Python package at: " << bundled_bmf_python; + BMFLOG(BMF_INFO) << "Added to PYTHONPATH: " << resources_dir; + } + bmf_python_check.close(); + } + } + } +#endif + + // Add Python site-packages to PYTHONPATH (App Python or Custom) + if (!python_site_packages.empty()) { + if (!bmf_output_path.empty()) { + bmf_output_path = python_site_packages + ":" + bmf_output_path; + } else { + bmf_output_path = python_site_packages; + } + } + + // Get current PYTHONPATH + std::string current_pythonpath; + const char* existing_pythonpath = std::getenv("PYTHONPATH"); + if (existing_pythonpath) { + current_pythonpath = existing_pythonpath; + } + + // Append BMF paths to PYTHONPATH + std::string new_pythonpath = bmf_lib_path + ":" + bmf_output_path; + if (!current_pythonpath.empty()) { + new_pythonpath += ":" + current_pythonpath; + } + + // Set PYTHONPATH environment variable + setenv("PYTHONPATH", new_pythonpath.c_str(), 1); + BMFLOG(BMF_INFO) << "Set PYTHONPATH: " << new_pythonpath; + + // Set BMF_MODULE_CONFIG_PATH to point to BUILTIN_CONFIG.json + setenv("BMF_MODULE_CONFIG_PATH", bmf_config_path.c_str(), 1); + BMFLOG(BMF_INFO) << "Set BMF_MODULE_CONFIG_PATH: " << bmf_config_path; + + return true; // Environment setup succeeded +} + +std::string TranscoderBMF::get_python_module_path() { + std::string module_path; + + // First check if BMF_MODULE_PATH environment variable is set + // This allows runtimes (AppImage, LingLong, Flatpak, etc.) to specify the module path + const char* env_module_path = getenv("BMF_MODULE_PATH"); + if (env_module_path && strlen(env_module_path) > 0) { + std::filesystem::path env_path(env_module_path); + if (std::filesystem::exists(env_path)) { + module_path = env_path.string(); + BMFLOG(BMF_INFO) << "Using BMF_MODULE_PATH from environment: " << module_path; + return module_path; + } else { + BMFLOG(BMF_WARNING) << "BMF_MODULE_PATH is set but path does not exist: " << env_module_path; + } + } + +#ifdef __APPLE__ + // For macOS app bundle + char exe_path[1024]; + uint32_t size = sizeof(exe_path); + if (_NSGetExecutablePath(exe_path, &size) == 0) { + char *real_path = realpath(exe_path, nullptr); + if (real_path) { + std::filesystem::path exe_dir = std::filesystem::path(real_path).parent_path(); + free(real_path); + + // Check if running from app bundle + // Path structure: OpenConverter.app/Contents/MacOS/OpenConverter + if (exe_dir.filename() == "MacOS") { + std::filesystem::path resources_dir = exe_dir.parent_path() / "Resources" / "modules"; + if (std::filesystem::exists(resources_dir)) { + module_path = resources_dir.string(); + BMFLOG(BMF_INFO) << "Using app bundle module path: " << module_path; + return module_path; + } + } + + // Check build directory (for development) + std::filesystem::path build_modules = exe_dir / "modules"; + if (std::filesystem::exists(build_modules)) { + module_path = build_modules.string(); + BMFLOG(BMF_INFO) << "Using build directory module path: " << module_path; + return module_path; + } + + // Check parent directory (for CLI build) + std::filesystem::path parent_modules = exe_dir.parent_path() / "modules"; + if (std::filesystem::exists(parent_modules)) { + module_path = parent_modules.string(); + BMFLOG(BMF_INFO) << "Using parent directory module path: " << module_path; + return module_path; + } + } + } +#else + // For Linux/Windows + // Try current directory first + try { + std::filesystem::path current_modules = std::filesystem::current_path() / "modules"; + if (std::filesystem::exists(current_modules)) { + module_path = current_modules.string(); + BMFLOG(BMF_INFO) << "Using current directory module path: " << module_path; + return module_path; + } + } catch (const std::filesystem::filesystem_error& e) { + BMFLOG(BMF_WARNING) << "Failed to get current directory: " << e.what(); + } +#endif + + // Fallback: use a safe default path + try { + module_path = std::filesystem::current_path().string(); + BMFLOG(BMF_WARNING) << "Module path not found, using current directory: " << module_path; + } catch (const std::filesystem::filesystem_error& e) { + // If we can't get current directory, use /tmp as last resort + module_path = "/tmp"; + BMFLOG(BMF_ERROR) << "Failed to get current directory: " << e.what(); + BMFLOG(BMF_ERROR) << "Using fallback path: " << module_path; + } + return module_path; +} + bmf_sdk::CBytes TranscoderBMF::decoder_callback(bmf_sdk::CBytes input) { std::string str_info; str_info.assign(reinterpret_cast(input.buffer), input.size); @@ -60,7 +413,7 @@ bmf_sdk::CBytes TranscoderBMF::encoder_callback(bmf_sdk::CBytes input) { } } else { - BMFLOG(BMF_WARNING) << "Failed to extract frame number"; + BMFLOG(BMF_WARNING) << "Failed to extract frame number from: " << str_info; } uint8_t bytes[] = {97, 98, 99, 100, 101, 0}; @@ -70,13 +423,13 @@ bmf_sdk::CBytes TranscoderBMF::encoder_callback(bmf_sdk::CBytes input) { bool TranscoderBMF::prepare_info(std::string input_path, std::string output_path) { // decoder init - if (encode_parameter->get_video_codec_name() == "") { + if (encode_parameter->get_video_codec_name() == "copy") { copy_video = true; } else { copy_video = false; } - if (encode_parameter->get_audio_codec_name() == "") { + if (encode_parameter->get_audio_codec_name() == "copy") { copy_audio = true; } else { copy_audio = false; @@ -103,7 +456,11 @@ bool TranscoderBMF::prepare_info(std::string input_path, nlohmann::json video_params = nlohmann::json::object(); // Always add codec and bitrate - video_params["codec"] = encode_parameter->get_video_codec_name(); + std::string video_codec_name = encode_parameter->get_video_codec_name(); + if (!video_codec_name.empty()) + video_params["codec"] = video_codec_name; + else + video_params["codec"] = "libx264"; video_params["bit_rate"] = encode_parameter->get_video_bit_rate(); // Only add width if it's set (> 0) @@ -132,7 +489,11 @@ bool TranscoderBMF::prepare_info(std::string input_path, // Build audio_params object nlohmann::json audio_params = nlohmann::json::object(); - audio_params["codec"] = encode_parameter->get_audio_codec_name(); + std::string audio_codec_name = encode_parameter->get_audio_codec_name(); + if (!audio_codec_name.empty()) + audio_params["codec"] = audio_codec_name; + else + audio_params["codec"] = "aac"; audio_params["bit_rate"] = encode_parameter->get_audio_bit_rate(); encoder_para = {{"output_path", output_path}, @@ -142,18 +503,93 @@ bool TranscoderBMF::prepare_info(std::string input_path, } bool TranscoderBMF::transcode(std::string input_path, std::string output_path) { + // Set up Python environment (PYTHONPATH) for BMF Python modules + // Returns false if App Python is not installed (no fallback to system Python) + if (!setup_python_environment()) { + BMFLOG(BMF_ERROR) << "Failed to set up Python environment. Transcoding aborted."; + return false; + } + + // Set a valid working directory to prevent BMF's internal getcwd() calls from failing + // When app is launched from Finder, there's no valid current working directory + try { + std::filesystem::current_path(); // Test if current path is valid + } catch (const std::filesystem::filesystem_error& e) { + // Current directory is invalid, set to a safe location + try { +#ifdef __APPLE__ + // Try to use app bundle's Resources directory + char exe_path[1024]; + uint32_t size = sizeof(exe_path); + if (_NSGetExecutablePath(exe_path, &size) == 0) { + char *real_path = realpath(exe_path, nullptr); + if (real_path) { + std::filesystem::path exe_dir = std::filesystem::path(real_path).parent_path(); + free(real_path); + + // If in app bundle, use Resources directory + if (exe_dir.filename() == "MacOS") { + std::filesystem::path resources_dir = exe_dir.parent_path() / "Resources"; + if (std::filesystem::exists(resources_dir)) { + std::filesystem::current_path(resources_dir); + BMFLOG(BMF_INFO) << "Set working directory to: " << resources_dir.string(); + } else { + // Fallback to /tmp + std::filesystem::current_path("/tmp"); + BMFLOG(BMF_INFO) << "Set working directory to: /tmp"; + } + } else { + // Not in app bundle, use executable directory + std::filesystem::current_path(exe_dir); + BMFLOG(BMF_INFO) << "Set working directory to: " << exe_dir.string(); + } + } + } +#else + // For Linux/Windows, use /tmp or C:\Temp + std::filesystem::current_path("/tmp"); + BMFLOG(BMF_INFO) << "Set working directory to: /tmp"; +#endif + } catch (const std::filesystem::filesystem_error& e2) { + BMFLOG(BMF_ERROR) << "Failed to set working directory: " << e2.what(); + // Continue anyway, BMF might still work + } + } prepare_info(input_path, output_path); int scheduler_cnt = 0; + AlgoMode algo_mode = encode_parameter->get_algo_mode(); + bmf::builder::Node *algo_node = nullptr; auto graph = bmf::builder::Graph(bmf::builder::NormalMode); auto decoder = - graph.Decode(bmf_sdk::JsonParam(decoder_para), "", scheduler_cnt++); + graph.Decode(bmf_sdk::JsonParam(decoder_para), "", scheduler_cnt); + + if (algo_mode == AlgoMode::Upscale) { + int upscale_factor = encode_parameter->get_upscale_factor(); + std::string module_path = get_python_module_path(); + + nlohmann::json enhance_option = { + {"fp32", true}, + {"output_scale", upscale_factor}, + }; + + BMFLOG(BMF_INFO) << "Loading enhance module from: " << module_path; + + algo_node = new bmf::builder::Node( + graph.Module({decoder["video"]}, "", bmf::builder::Python, + bmf_sdk::JsonParam(enhance_option), "", + module_path, + "enhance_module.EnhanceModule", + bmf::builder::Immediate, + scheduler_cnt)); + } auto encoder = - graph.Encode(decoder["video"], decoder["audio"], - bmf_sdk::JsonParam(encoder_para), "", scheduler_cnt++); + graph.Encode(algo_mode == AlgoMode::Upscale ? *algo_node : decoder["video"], + decoder["audio"], + bmf_sdk::JsonParam(encoder_para), "", scheduler_cnt); auto de_callback = std::bind(&TranscoderBMF::decoder_callback, this, std::placeholders::_1); @@ -162,15 +598,23 @@ bool TranscoderBMF::transcode(std::string input_path, std::string output_path) { decoder.AddCallback( 0, std::function(de_callback)); - encoder.AddCallback( - 0, std::function(en_callback)); + if (algo_mode != AlgoMode::None) { + algo_node->AddCallback( + 0, std::function(en_callback)); + } else { + encoder.AddCallback( + 0, std::function(en_callback)); + } nlohmann::json graph_para = {{"dump_graph", 1}}; graph.SetOption(bmf_sdk::JsonParam(graph_para)); - if (graph.Run() == 0) { - return true; - } else { - return false; + int result = graph.Run(); + + // Clean up allocated memory + if (algo_node != nullptr) { + delete algo_node; } + + return (result == 0); } diff --git a/src/transcoder/src/transcoder_fftool.cpp b/src/transcoder/src/transcoder_fftool.cpp index 6cec14a1..eab73915 100644 --- a/src/transcoder/src/transcoder_fftool.cpp +++ b/src/transcoder/src/transcoder_fftool.cpp @@ -53,13 +53,13 @@ std::string escape_windows_path(const std::string &path) { bool TranscoderFFTool::prepared_opt() { - if (encode_parameter->get_video_codec_name() == "") { + if (encode_parameter->get_video_codec_name() == "copy") { copy_video = true; } else { copy_video = false; } - if (encode_parameter->get_audio_codec_name() == "") { + if (encode_parameter->get_audio_codec_name() == "copy") { copy_audio = true; } else { copy_audio = false; diff --git a/tool/download_models.sh b/tool/download_models.sh new file mode 100755 index 00000000..83798f17 --- /dev/null +++ b/tool/download_models.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# Copyright 2025 Jack Lau +# Email: jacklau1222gm@gmail.com +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Script to download AI model weights for OpenConverter +# This script is called during the build process to download required model files + +set -e # Exit on error + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +WEIGHTS_DIR="$PROJECT_ROOT/src/modules/weights" + +# Create weights directory if it doesn't exist +mkdir -p "$WEIGHTS_DIR" + +echo "=========================================" +echo "Downloading AI Model Weights" +echo "=========================================" + +# Define models as separate arrays (compatible with older bash versions) +MODEL_NAME="realesr-animevideov3.pth" +MODEL_URL="https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.5.0/realesr-animevideov3.pth" +MODEL_PATH="$WEIGHTS_DIR/$MODEL_NAME" + +if [ -f "$MODEL_PATH" ]; then + echo "✓ $MODEL_NAME already exists, skipping download" +else + echo "⬇ Downloading $MODEL_NAME..." + if command -v curl &> /dev/null; then + curl -L -o "$MODEL_PATH" "$MODEL_URL" + elif command -v wget &> /dev/null; then + wget -O "$MODEL_PATH" "$MODEL_URL" + else + echo "Error: Neither curl nor wget is available. Please install one of them." + exit 1 + fi + + if [ -f "$MODEL_PATH" ]; then + echo "✓ Successfully downloaded $MODEL_NAME" + else + echo "✗ Failed to download $MODEL_NAME" + exit 1 + fi +fi + +echo "=========================================" +echo "All model weights are ready!" +echo "=========================================" diff --git a/tool/fix_macos_libs.sh b/tool/fix_macos_libs.sh index 82fd48ee..17409447 100755 --- a/tool/fix_macos_libs.sh +++ b/tool/fix_macos_libs.sh @@ -14,14 +14,25 @@ NC='\033[0m' # No Color echo -e "${GREEN}=== OpenConverter macOS Library Fixer ===${NC}" -# Check if app bundle exists -if [ ! -d "build/OpenConverter.app" ]; then - echo -e "${RED}Error: OpenConverter.app not found in build/ directory${NC}" +# Auto-detect build directory +BUILD_DIR="" +if [ -d "build-release/OpenConverter.app" ]; then + BUILD_DIR="build-release" +elif [ -d "build/OpenConverter.app" ]; then + BUILD_DIR="build" +elif [ -d "../build-release/OpenConverter.app" ]; then + BUILD_DIR="../build-release" +elif [ -d "../build/OpenConverter.app" ]; then + BUILD_DIR="../build" +else + echo -e "${RED}Error: OpenConverter.app not found${NC}" + echo "Searched in: build-release/, build/, ../build-release/, ../build/" echo "Please build the app first with: cd src && cmake -B build && cd build && make" exit 1 fi -cd build +echo -e "${GREEN}Found app bundle in: $BUILD_DIR${NC}" +cd "$BUILD_DIR" APP_DIR="OpenConverter.app" APP_FRAMEWORKS="$APP_DIR/Contents/Frameworks" @@ -48,6 +59,66 @@ fi FFMPEG_LIB_DIR="$FFMPEG_PREFIX/lib" +echo -e "${YELLOW}Step 2.5: Bundling BMF libraries (if available)...${NC}" +# Check if BMF_ROOT_PATH is set +if [ -n "$BMF_ROOT_PATH" ]; then + # If BMF_ROOT_PATH doesn't end with /output/bmf, append it (same logic as CMake) + if [[ ! "$BMF_ROOT_PATH" =~ output/bmf$ ]]; then + BMF_ROOT_PATH="$BMF_ROOT_PATH/output/bmf" + fi + + if [ -d "$BMF_ROOT_PATH" ]; then + echo -e "${GREEN}Found BMF at: $BMF_ROOT_PATH${NC}" + + # Create lib/ subdirectory for builtin modules (BMF hardcoded path) + mkdir -p "$APP_FRAMEWORKS/lib" + else + echo -e "${YELLOW}BMF_ROOT_PATH set but directory not found: $BMF_ROOT_PATH${NC}" + echo " Skipping BMF bundling" + BMF_ROOT_PATH="" + fi +else + echo -e "${YELLOW}BMF_ROOT_PATH not set, skipping BMF bundling${NC}" + echo " To bundle BMF libraries, set: export BMF_ROOT_PATH=/path/to/bmf" +fi + +if [ -n "$BMF_ROOT_PATH" ] && [ -d "$BMF_ROOT_PATH" ]; then + + # Copy builtin modules to lib/ subdirectory + echo " Copying BMF builtin modules to Frameworks/lib/..." + for module in libbuiltin_modules.dylib libcopy_module.dylib libcvtcolor.dylib; do + if [ -f "$BMF_ROOT_PATH/lib/$module" ]; then + cp "$BMF_ROOT_PATH/lib/$module" "$APP_FRAMEWORKS/lib/" 2>/dev/null || true + chmod +w "$APP_FRAMEWORKS/lib/$module" 2>/dev/null || true + echo " Copied: $module" + fi + done + + # Copy other BMF libraries to Frameworks/ + echo " Copying BMF core libraries to Frameworks/..." + for lib in libbmf_py_loader.dylib libbmf_module_sdk.dylib libengine.dylib libhmp.dylib _bmf.cpython-39-darwin.so _hmp.cpython-39-darwin.so; do + if [ -f "$BMF_ROOT_PATH/lib/$lib" ]; then + cp "$BMF_ROOT_PATH/lib/$lib" "$APP_FRAMEWORKS/" 2>/dev/null || true + chmod +w "$APP_FRAMEWORKS/$lib" 2>/dev/null || true + echo " Copied: $lib" + fi + done + + # Copy BMF config + if [ -f "$BMF_ROOT_PATH/BUILTIN_CONFIG.json" ]; then + cp "$BMF_ROOT_PATH/BUILTIN_CONFIG.json" "$APP_FRAMEWORKS/" 2>/dev/null || true + echo " Copied: BUILTIN_CONFIG.json" + fi + + # Bundle BMF Python package to Resources/bmf/ (named 'bmf' for Python import) + echo " Copying BMF Python package to Resources/bmf/..." + rm -rf "$APP_DIR/Contents/Resources/bmf" + cp -R "$BMF_ROOT_PATH" "$APP_DIR/Contents/Resources/bmf" 2>/dev/null || true + echo " Copied BMF Python package as 'bmf'" + + echo -e "${GREEN}BMF libraries bundled successfully${NC}" +fi + echo -e "${YELLOW}Step 3: Checking if dylibbundler is available...${NC}" if ! command -v dylibbundler &> /dev/null; then echo -e "${YELLOW}dylibbundler not found, installing via Homebrew...${NC}" @@ -75,8 +146,8 @@ copy_lib_if_needed() { for iteration in 1 2 3; do echo "Pass $iteration: Scanning for missing dependencies..." - # Get all dylib files in Frameworks folder - ALL_LIBS=$(find "$APP_FRAMEWORKS" -name "*.dylib" -type f) + # Get all dylib and .so files in Frameworks folder (including BMF Python modules) + ALL_LIBS=$(find "$APP_FRAMEWORKS" -type f \( -name "*.dylib" -o -name "*.so" \)) new_libs_copied=0 @@ -129,9 +200,9 @@ for iteration in 1 2 3; do new_libs_copied=$((new_libs_copied + 1)) fi elif [[ "$dep" == @rpath/* ]]; then - # @rpath - try Homebrew locations + # @rpath - try Homebrew and BMF locations dep_basename=$(basename "$dep") - # Search in multiple Homebrew locations + # Search in multiple Homebrew and BMF locations search_dirs=( "$FFMPEG_LIB_DIR" "$(brew --prefix)/lib" @@ -141,6 +212,10 @@ for iteration in 1 2 3; do "$(brew --prefix x264 2>/dev/null)/lib" "$(brew --prefix x265 2>/dev/null)/lib" ) + # Add BMF library directories if BMF_ROOT_PATH is set + if [ -n "$BMF_ROOT_PATH" ]; then + search_dirs+=("$BMF_ROOT_PATH/lib") + fi for search_dir in "${search_dirs[@]}"; do if [ -z "$search_dir" ]; then continue