diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..654acf4 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,166 @@ +name: Create Platform-Specific Artifacts + +on: + release: + types: [ published, created, edited, prereleased ] + + workflow_dispatch: + inputs: + release_tag: + description: 'Release tag to build artifacts for' + required: false + type: string + +jobs: + prepare-artifacts: + runs-on: ubuntu-latest + + outputs: + platforms: ${{ steps.generate-matrix.outputs.platforms }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.release_tag || github.ref }} + + - name: Install yq + run: | + wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/local/bin/yq + chmod +x /usr/local/bin/yq + + - name: Generate Build Matrix + id: generate-matrix + run: | + # Extract platforms from platforms.yml + platforms=$(yq -o=json 'keys' platforms.yml) + echo "platforms=$(echo $platforms | jq -c '. | map(split("/")[0]) | unique')" >> $GITHUB_OUTPUT + + build-artifacts: + needs: prepare-artifacts + + runs-on: ubuntu-latest + + strategy: + matrix: + platform: ${{ fromJson(needs.prepare-artifacts.outputs.platforms) }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.release_tag || github.ref }} + + - name: Install yq + run: | + wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/local/bin/yq + chmod +x /usr/local/bin/yq + + - name: Create Platform-Specific Artifact + run: | + # Prepare exclusion list + if [ -f ".gitignore" ]; then + cp .gitignore rsync_exclude.txt + else + touch rsync_exclude.txt + fi + + # Add platform-specific exclusions from platforms.yml + echo "" >> rsync_exclude.txt + yq e ".\"${{ matrix.platform }}\".exclude[]" platforms.yml >> rsync_exclude.txt + + # Add standard exclusions + echo ".git" >> rsync_exclude.txt + echo ".github" >> rsync_exclude.txt + echo "rsync_exclude.txt" >> rsync_exclude.txt + + # Copy files using rsync with exclusions + DIST_DIR="dist-${{ matrix.platform }}" + mkdir -p "$DIST_DIR" + echo "$DIST_DIR" >> rsync_exclude.txt + rsync -av --exclude-from=rsync_exclude.txt ./ "$DIST_DIR"/ + + # Compress artifact + if [[ "${{ matrix.platform }}" == windows-* ]]; then + zip -r "dist-${{ matrix.platform }}.zip" "$DIST_DIR" + ARTIFACT_EXT="zip" + else + tar -czvf "dist-${{ matrix.platform }}.tar.gz" "$DIST_DIR" + ARTIFACT_EXT="tar.gz" + fi + + echo "Artifact created: dist-${{ matrix.platform }}.$ARTIFACT_EXT" + + - name: Upload Artifact to Release + uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') + with: + files: dist-* + + - name: Upload Artifact to Artifact Hub + uses: actions/upload-artifact@v4 + with: + name: dist-${{ matrix.platform }} + path: dist-* + retention-days: 1 + + create-release-metadata: + needs: [ prepare-artifacts, build-artifacts ] + + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.release_tag || github.ref }} + + - name: Install yq + run: | + wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/local/bin/yq + chmod +x /usr/local/bin/yq + + - name: Generate Platform URLs Metadata + run: | + # Prepare variables + RELEASE_TAG="${{ github.event.inputs.release_tag || github.ref }}" + REPO="${{ github.repository }}" + + # Start JSON structure + echo "{" > platform-urls.json + echo ' "platform-urls": {' >> platform-urls.json + + # Generate URLs for each platform + FIRST=true + platforms=$(yq -o=json 'keys' platforms.yml | jq -r '.[]') + for platform in $platforms; do + if [ "$FIRST" = true ]; then + FIRST=false + else + echo "," >> platform-urls.json + fi + + # Determine file extension + if [[ "$platform" == windows-* ]]; then + EXT="zip" + else + EXT="tar.gz" + fi + + # Generate URL + printf ' "%s": "https://github.com/%s/releases/download/%s/dist-%s.%s"' \ + "$platform" "$REPO" "$RELEASE_TAG" "$platform" "$EXT" >> platform-urls.json + done + + # Close JSON structure + echo "" >> platform-urls.json + echo ' }' >> platform-urls.json + echo "}" >> platform-urls.json + + cat platform-urls.json + + - name: Upload Platform URLs Metadata + uses: softprops/action-gh-release@v2 + if: startsWith(github.ref, 'refs/tags/') + with: + files: platform-urls.json diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index ee9d6bc..ca81a72 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -11,6 +11,7 @@ jobs: matrix: os: [ubuntu-latest, macos-latest, windows-latest] php: [8.1, 8.2, 8.3, 8.4] + max-parallel: 4 name: Tests PHP${{ matrix.php }} - ${{ matrix.os }} diff --git a/composer.json b/composer.json index fa962b4..292c563 100644 --- a/composer.json +++ b/composer.json @@ -1,12 +1,13 @@ { "name": "codewithkyrian/whisper.php", "description": "PHP bindings for OpenAI Whisper made possible by whisper.cpp", - "type": "library", + "type": "platform-package", "version": "1.0.0", "require": { "php": "^8.1", "ext-ffi": "*", - "psr/log": "^3.0" + "psr/log": "^3.0", + "codewithkyrian/platform-package-installer": "^1.0" }, "require-dev": { "symfony/var-dumper": "^6.4.11|^7.1.5", @@ -35,7 +36,18 @@ ], "config": { "allow-plugins": { - "pestphp/pest-plugin": true + "pestphp/pest-plugin": true, + "codewithkyrian/platform-package-installer": true + } + }, + "extra": { + "platform-urls": { + "linux-x86_64": "https://github.com/Codewithkyrian/whisper.php/releases/download/{version}/dist-linux-x86_64.tar.gz", + "linux-arm64": "https://github.com/Codewithkyrian/whisper.php/releases/download/{version}/dist-linux-arm64.tar.gz", + "darwin-x86_64": "https://github.com/Codewithkyrian/whisper.php/releases/download/{version}/dist-darwin-x86_64.tar.gz", + "darwin-arm64": "https://github.com/Codewithkyrian/whisper.php/releases/download/{version}/dist-darwin-arm64.tar.gz", + "windows-x86_64": "https://github.com/Codewithkyrian/whisper.php/releases/download/{version}/dist-windows-x86_64.zip", + "windows-arm64": "https://github.com/Codewithkyrian/whisper.php/releases/download/{version}/dist-windows-arm64.zip" } } } diff --git a/lib/.gitignore b/lib/.gitignore deleted file mode 100644 index 33662f5..0000000 --- a/lib/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/* diff --git a/lib/darwin-arm64/libggml-base.dylib b/lib/darwin-arm64/libggml-base.dylib new file mode 100644 index 0000000..771eb5c Binary files /dev/null and b/lib/darwin-arm64/libggml-base.dylib differ diff --git a/lib/darwin-arm64/libggml-blas.dylib b/lib/darwin-arm64/libggml-blas.dylib new file mode 100644 index 0000000..a885c40 Binary files /dev/null and b/lib/darwin-arm64/libggml-blas.dylib differ diff --git a/lib/darwin-arm64/libggml-cpu.dylib b/lib/darwin-arm64/libggml-cpu.dylib new file mode 100644 index 0000000..5a70c33 Binary files /dev/null and b/lib/darwin-arm64/libggml-cpu.dylib differ diff --git a/lib/darwin-arm64/libggml-metal.dylib b/lib/darwin-arm64/libggml-metal.dylib new file mode 100644 index 0000000..16f05d0 Binary files /dev/null and b/lib/darwin-arm64/libggml-metal.dylib differ diff --git a/lib/darwin-arm64/libggml.dylib b/lib/darwin-arm64/libggml.dylib new file mode 100644 index 0000000..a1ca050 Binary files /dev/null and b/lib/darwin-arm64/libggml.dylib differ diff --git a/lib/darwin-arm64/libsamplerate.dylib b/lib/darwin-arm64/libsamplerate.dylib new file mode 100644 index 0000000..d68e8d7 Binary files /dev/null and b/lib/darwin-arm64/libsamplerate.dylib differ diff --git a/lib/darwin-arm64/libsndfile.dylib b/lib/darwin-arm64/libsndfile.dylib new file mode 100644 index 0000000..144af7c Binary files /dev/null and b/lib/darwin-arm64/libsndfile.dylib differ diff --git a/lib/darwin-arm64/libwhisper.dylib b/lib/darwin-arm64/libwhisper.dylib new file mode 100644 index 0000000..4debb02 Binary files /dev/null and b/lib/darwin-arm64/libwhisper.dylib differ diff --git a/lib/darwin-x86_64/libggml-base.dylib b/lib/darwin-x86_64/libggml-base.dylib new file mode 100755 index 0000000..771eb5c Binary files /dev/null and b/lib/darwin-x86_64/libggml-base.dylib differ diff --git a/lib/darwin-x86_64/libggml-blas.dylib b/lib/darwin-x86_64/libggml-blas.dylib new file mode 100755 index 0000000..a885c40 Binary files /dev/null and b/lib/darwin-x86_64/libggml-blas.dylib differ diff --git a/lib/darwin-x86_64/libggml-cpu.dylib b/lib/darwin-x86_64/libggml-cpu.dylib new file mode 100755 index 0000000..5a70c33 Binary files /dev/null and b/lib/darwin-x86_64/libggml-cpu.dylib differ diff --git a/lib/darwin-x86_64/libggml-metal.dylib b/lib/darwin-x86_64/libggml-metal.dylib new file mode 100755 index 0000000..16f05d0 Binary files /dev/null and b/lib/darwin-x86_64/libggml-metal.dylib differ diff --git a/lib/darwin-x86_64/libggml.dylib b/lib/darwin-x86_64/libggml.dylib new file mode 100755 index 0000000..a1ca050 Binary files /dev/null and b/lib/darwin-x86_64/libggml.dylib differ diff --git a/lib/darwin-x86_64/libsamplerate.dylib b/lib/darwin-x86_64/libsamplerate.dylib new file mode 100755 index 0000000..c9179ec Binary files /dev/null and b/lib/darwin-x86_64/libsamplerate.dylib differ diff --git a/lib/darwin-x86_64/libsndfile.dylib b/lib/darwin-x86_64/libsndfile.dylib new file mode 100644 index 0000000..02d410c Binary files /dev/null and b/lib/darwin-x86_64/libsndfile.dylib differ diff --git a/lib/darwin-x86_64/libwhisper.dylib b/lib/darwin-x86_64/libwhisper.dylib new file mode 100644 index 0000000..4debb02 Binary files /dev/null and b/lib/darwin-x86_64/libwhisper.dylib differ diff --git a/lib/linux-arm64/libggml.so b/lib/linux-arm64/libggml.so new file mode 100755 index 0000000..7734dd0 Binary files /dev/null and b/lib/linux-arm64/libggml.so differ diff --git a/lib/linux-arm64/libsamplerate.so b/lib/linux-arm64/libsamplerate.so new file mode 100644 index 0000000..02bbc35 Binary files /dev/null and b/lib/linux-arm64/libsamplerate.so differ diff --git a/lib/linux-arm64/libsndfile.so b/lib/linux-arm64/libsndfile.so new file mode 100644 index 0000000..03aad69 Binary files /dev/null and b/lib/linux-arm64/libsndfile.so differ diff --git a/lib/linux-arm64/libwhisper.so b/lib/linux-arm64/libwhisper.so new file mode 100755 index 0000000..9a3c50f Binary files /dev/null and b/lib/linux-arm64/libwhisper.so differ diff --git a/lib/linux-x86_64/libggml.so b/lib/linux-x86_64/libggml.so new file mode 100755 index 0000000..956d543 Binary files /dev/null and b/lib/linux-x86_64/libggml.so differ diff --git a/lib/linux-x86_64/libsamplerate.so b/lib/linux-x86_64/libsamplerate.so new file mode 100644 index 0000000..4ceb149 Binary files /dev/null and b/lib/linux-x86_64/libsamplerate.so differ diff --git a/lib/linux-x86_64/libsndfile.so b/lib/linux-x86_64/libsndfile.so new file mode 100644 index 0000000..01b3afa Binary files /dev/null and b/lib/linux-x86_64/libsndfile.so differ diff --git a/lib/linux-x86_64/libwhisper.so b/lib/linux-x86_64/libwhisper.so new file mode 100755 index 0000000..f5f069d Binary files /dev/null and b/lib/linux-x86_64/libwhisper.so differ diff --git a/lib/windows-x86_64/ggml-whisper.dll b/lib/windows-x86_64/ggml-whisper.dll new file mode 100644 index 0000000..ba8f127 Binary files /dev/null and b/lib/windows-x86_64/ggml-whisper.dll differ diff --git a/lib/windows-x86_64/libsamplerate.dll b/lib/windows-x86_64/libsamplerate.dll new file mode 100644 index 0000000..6bbb805 Binary files /dev/null and b/lib/windows-x86_64/libsamplerate.dll differ diff --git a/lib/windows-x86_64/libsndfile.dll b/lib/windows-x86_64/libsndfile.dll new file mode 100644 index 0000000..d548a8f Binary files /dev/null and b/lib/windows-x86_64/libsndfile.dll differ diff --git a/lib/windows-x86_64/libwhisper.dll b/lib/windows-x86_64/libwhisper.dll new file mode 100644 index 0000000..ccc8f04 Binary files /dev/null and b/lib/windows-x86_64/libwhisper.dll differ diff --git a/platforms.yml b/platforms.yml new file mode 100644 index 0000000..5caff5c --- /dev/null +++ b/platforms.yml @@ -0,0 +1,55 @@ +linux-x86_64: + exclude: + - libs/linux-arm64 + - libs/darwin-* + - libs/windows-* + - "*.exe" + - "*.dylib" + - "**/*.windows.*" + +linux-arm64: + exclude: + - libs/linux-x86_64 + - libs/windows-* + - libs/darwin-* + - "*.exe" + - "*.dylib" + - "**/*.windows.*" + +darwin-x86_64: + exclude: + - libs/darwin-arm64 + - libs/windows-* + - libs/linux-* + - "*.exe" + - "*.so" + - "**/*.linux.*" + +darwin-arm64: + exclude: + - libs/darwin-x86_64 + - libs/windows-* + - libs/linux-* + - "*.exe" + - "*.so" + - "**/*.linux.*" + +windows-x86_64: + exclude: + - libs/windows-arm64 + - libs/linux-* + - libs/darwin-* + - "*.so" + - "*.dylib" + - "**/*.darwin.*" + - "**/*.linux.*" + +windows-arm64: + exclude: + - libs/windows-x86_64 + - libs/linux-* + - libs/darwin-* + - "*.so" + - "*.dylib" + - "**/*.darwin.*" + - "**/*.linux.*" diff --git a/src/LibraryLoader.php b/src/LibraryLoader.php index 48aa37e..21f4700 100644 --- a/src/LibraryLoader.php +++ b/src/LibraryLoader.php @@ -4,36 +4,31 @@ namespace Codewithkyrian\Whisper; +use Codewithkyrian\PlatformPackageInstaller\Platform; use FFI; use RuntimeException; -use ZipArchive; class LibraryLoader { private const LIBRARY_CONFIGS = [ - 'whisper' => [ - 'header' => 'whisper.h', - 'lib_prefix' => 'libwhisper', - ], - 'sndfile' => [ - 'header' => 'sndfile.h', - 'lib_prefix' => 'libsndfile', - ], - 'samplerate' => [ - 'header' => 'samplerate.h', - 'lib_prefix' => 'libsamplerate', - ], + 'whisper' => ['header' => 'whisper.h', 'library' => 'libwhisper'], + 'sndfile' => ['header' => 'sndfile.h', 'library' => 'libsndfile',], + 'samplerate' => ['header' => 'samplerate.h', 'library' => 'libsamplerate'], + ]; + + private const PLATFORM_CONFIGS = [ + 'linux-x86_64' => ['directory' => 'linux-x86_64', 'extension' => 'so',], + 'linux-arm64' => ['directory' => 'linux-arm64', 'extension' => 'so',], + 'darwin-x86_64' => ['directory' => 'darwin-x86_64', 'extension' => 'dylib',], + 'darwin-arm64' => ['directory' => 'darwin-arm64', 'extension' => 'dylib',], + 'windows-x86_64' => ['directory' => 'windows-x86_64', 'extension' => 'dll',], ]; - private const WHISPER_CPP_VERSION = '1.7.2'; - private const DOWNLOAD_URL = 'https://huggingface.co/codewithkyrian/whisper.php/resolve/%s/libs/%s.zip'; private static array $instances = []; - private ?PlatformDetector $platformDetector; private ?FFI $kernel32 = null; - public function __construct(?PlatformDetector $platformDetector = null) + public function __construct() { - $this->platformDetector = $platformDetector ?? new PlatformDetector(); $this->addDllDirectory(); } @@ -63,20 +58,16 @@ private function load(string $library): FFI throw new RuntimeException("Unsupported library: {$library}"); } - $config = self::LIBRARY_CONFIGS[$library]; - - $headerPath = self::getHeaderPath($config['header']); - $libPath = self::getLibraryPath( - $config['lib_prefix'], - $this->platformDetector->getLibraryExtension(), - $this->platformDetector->getPlatformIdentifier() - ); - - if (!file_exists($libPath)) { - $this->downloadLibraries(); + $platformConfig = Platform::findBestMatch(self::PLATFORM_CONFIGS); + if (!$platformConfig) { + throw new RuntimeException("No matching platform configuration found"); } - return FFI::cdef(file_get_contents($headerPath), $libPath); + $config = self::LIBRARY_CONFIGS[$library]; + $headerPath = $this->getHeaderPath($config['header']); + $libraryPath = $this->getLibraryPath($config['library'], $platformConfig['extension'], $platformConfig['directory']); + + return FFI::cdef(file_get_contents($headerPath), $libraryPath); } private static function getHeaderPath(string $headerFile): string @@ -84,57 +75,17 @@ private static function getHeaderPath(string $headerFile): string return self::joinPaths(dirname(__DIR__), 'include', $headerFile); } - private static function getLibraryPath(string $prefix, string $extension, string $platform): string - { - return self::joinPaths(self::getLibraryDirectory($platform), "$prefix.$extension"); - } - - private static function getLibraryDirectory(string $platform): string - { - return self::joinPaths(dirname(__DIR__), 'lib', $platform); - } - /** - * Download libraries from Hugging Face + * Get path to library file */ - private function downloadLibraries(): void + private function getLibraryPath(string $libName, string $extension, string $platformDir): string { - $platform = $this->platformDetector->getPlatformIdentifier(); - - $url = sprintf(self::DOWNLOAD_URL, self::WHISPER_CPP_VERSION, $platform); - - $tempFile = tempnam(sys_get_temp_dir(), 'whisper-cpp-libs'); - - $ch = curl_init(); - $fp = fopen($tempFile, 'w'); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_FILE, $fp); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); - curl_setopt($ch, CURLOPT_FAILONERROR, true); - curl_setopt($ch, CURLOPT_HTTPHEADER, [ - 'Accept: application/octet-stream', - ]); - - if (!curl_exec($ch)) { - fclose($fp); - unlink($tempFile); - throw new \RuntimeException(sprintf('Failed to download libraries from %s: %s', $url, curl_error($ch))); - } - - // Extract ZIP file - $zip = new ZipArchive; - if ($zip->open($tempFile) === true) { - $platformLibDir = self::joinPaths(dirname(__DIR__), 'lib', $platform); - if (!is_dir($platformLibDir)) { - mkdir($platformLibDir, 0755, true); - } - $zip->extractTo($platformLibDir); - $zip->close(); + return self::joinPaths(dirname(__DIR__), 'lib', $platformDir, "$libName.$extension"); + } - unlink($tempFile); - } else { - throw new RuntimeException('Failed to downloaded ZIP'); - } + private static function getLibraryDirectory(string $platformDir): string + { + return self::joinPaths(dirname(__DIR__), 'lib', $platformDir); } /** @@ -142,15 +93,17 @@ private function downloadLibraries(): void */ private function addDllDirectory(): void { - if (!$this->platformDetector->isWindows()) return; + if (!Platform::isWindows()) return; + + $platformConfig = Platform::findBestMatch(self::PLATFORM_CONFIGS); + $libraryDir = self::getLibraryDirectory($platformConfig['directory']); - $libDir = ($this->getLibraryDirectory($this->platformDetector->getPlatformIdentifier())); $this->kernel32 ??= FFI::cdef(" int SetDllDirectoryA(const char* lpPathName); int SetDefaultDllDirectories(unsigned long DirectoryFlags); ", 'kernel32.dll'); - $this->kernel32->SetDllDirectoryA($libDir); + $this->kernel32->SetDllDirectoryA($libraryDir); } /** diff --git a/src/PlatformDetector.php b/src/PlatformDetector.php deleted file mode 100644 index adfd396..0000000 --- a/src/PlatformDetector.php +++ /dev/null @@ -1,67 +0,0 @@ - [ - 'x86_64' => ['so', 'linux', 'x86_64'], - 'aarch64' => ['so', 'linux', 'arm64'], - 'arm64' => ['so', 'linux', 'arm64'], - ], - 'darwin' => [ - 'x86_64' => ['dylib', 'darwin', 'x86_64'], - 'arm64' => ['dylib', 'darwin', 'arm64'], - ], - 'windows' => [ - 'x86_64' => ['dll', 'windows', 'x86_64'], - 'AMD64' => ['dll', 'windows', 'x86_64'], - ], - ]; - - private string $os; - - private string $arch; - - public function __construct() - { - $this->os = strtolower(PHP_OS_FAMILY); - $this->arch = php_uname('m'); - - if (! $this->isPlatformSupported()) { - throw new RuntimeException( - "Unsupported platform: {$this->os} {$this->arch}" - ); - } - } - - public function getLibraryExtension(): string - { - return self::SUPPORTED_PLATFORMS[$this->os][$this->arch][0]; - } - - public function getPlatformIdentifier(): string - { - $platform = self::SUPPORTED_PLATFORMS[$this->os][$this->arch]; - - return "{$platform[1]}-{$platform[2]}"; - } - - private function isPlatformSupported(): bool - { - return isset(self::SUPPORTED_PLATFORMS[$this->os][$this->arch]); - } - - public function isWindows(): bool - { - return $this->os === 'windows'; - } -}