diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index b3e101d..ee9d6bc 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -9,7 +9,7 @@ jobs: strategy: fail-fast: false matrix: - os: [macos-latest, macos-13, ubuntu-latest] + os: [ubuntu-latest, macos-latest, windows-latest] php: [8.1, 8.2, 8.3, 8.4] name: Tests PHP${{ matrix.php }} - ${{ matrix.os }} diff --git a/src/LibraryLoader.php b/src/LibraryLoader.php index 2037cfb..48aa37e 100644 --- a/src/LibraryLoader.php +++ b/src/LibraryLoader.php @@ -24,62 +24,59 @@ class LibraryLoader 'lib_prefix' => 'libsamplerate', ], ]; - 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; - private static ?PlatformDetector $platformDetector = null; + public function __construct(?PlatformDetector $platformDetector = null) + { + $this->platformDetector = $platformDetector ?? new PlatformDetector(); + $this->addDllDirectory(); + } + + public function __destruct() + { + $this->resetDllDirectory(); + } /** * Gets the FFI instance for the specified library */ - public static function getInstance(string $library): FFI + public function get(string $library): FFI { if (!isset(self::$instances[$library])) { - self::$instances[$library] = self::createFFIInstance($library); + self::$instances[$library] = $this->load($library); } return self::$instances[$library]; } /** - * Creates a new FFI instance for the specified library + * Loads a new FFI instance for the specified library */ - private static function createFFIInstance(string $library): FFI + private function load(string $library): FFI { if (!isset(self::LIBRARY_CONFIGS[$library])) { throw new RuntimeException("Unsupported library: {$library}"); } $config = self::LIBRARY_CONFIGS[$library]; - $detector = self::getPlatformDetector(); $headerPath = self::getHeaderPath($config['header']); $libPath = self::getLibraryPath( $config['lib_prefix'], - $detector->getLibraryExtension(), - $detector->getPlatformIdentifier() + $this->platformDetector->getLibraryExtension(), + $this->platformDetector->getPlatformIdentifier() ); if (!file_exists($libPath)) { - self::downloadLibraries(); - } - - return FFI::cdef( - file_get_contents($headerPath), - $libPath - ); - } - - private static function getPlatformDetector(): PlatformDetector - { - if (self::$platformDetector === null) { - self::$platformDetector = new PlatformDetector; + $this->downloadLibraries(); } - return self::$platformDetector; + return FFI::cdef(file_get_contents($headerPath), $libPath); } private static function getHeaderPath(string $headerFile): string @@ -89,16 +86,20 @@ private static function getHeaderPath(string $headerFile): string private static function getLibraryPath(string $prefix, string $extension, string $platform): string { - return self::joinPaths(dirname(__DIR__), 'lib', $platform, "$prefix.$extension"); + 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 */ - private static function downloadLibraries(): void + private function downloadLibraries(): void { - $detector = self::getPlatformDetector(); - $platform = $detector->getPlatformIdentifier(); + $platform = $this->platformDetector->getPlatformIdentifier(); $url = sprintf(self::DOWNLOAD_URL, self::WHISPER_CPP_VERSION, $platform); @@ -136,6 +137,32 @@ private static function downloadLibraries(): void } } + /** + * Add DLL directory to search path for Windows + */ + private function addDllDirectory(): void + { + if (!$this->platformDetector->isWindows()) return; + + $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); + } + + /** + * Reset DLL directory search path + */ + private function resetDllDirectory(): void + { + if ($this->kernel32 !== null) { + $this->kernel32->SetDllDirectoryA(null); + } + } + private static function joinPaths(string ...$args): string { $paths = []; diff --git a/src/PlatformDetector.php b/src/PlatformDetector.php index e4cf049..adfd396 100644 --- a/src/PlatformDetector.php +++ b/src/PlatformDetector.php @@ -59,4 +59,9 @@ private function isPlatformSupported(): bool { return isset(self::SUPPORTED_PLATFORMS[$this->os][$this->arch]); } + + public function isWindows(): bool + { + return $this->os === 'windows'; + } } diff --git a/src/Samplerate.php b/src/Samplerate.php index 89c44c0..e5eba16 100644 --- a/src/Samplerate.php +++ b/src/Samplerate.php @@ -12,6 +12,8 @@ class Samplerate { + private static ?LibraryLoader $loader = null; + /** * Returns an instance of the FFI class after checking if it has already been instantiated. * If not, it creates a new instance by defining the header contents and library path. @@ -22,7 +24,8 @@ class Samplerate */ public static function ffi(): FFI { - return LibraryLoader::getInstance('samplerate'); + self::$loader ??= new LibraryLoader; + return self::$loader->get('samplerate'); } /** diff --git a/src/Sndfile.php b/src/Sndfile.php index 73f9beb..c7be969 100644 --- a/src/Sndfile.php +++ b/src/Sndfile.php @@ -12,6 +12,7 @@ class Sndfile { + private static ?LibraryLoader $loader = null; /** * Returns an instance of the FFI class after checking if it has already been instantiated. * If not, it creates a new instance by defining the header contents and library path. @@ -22,7 +23,8 @@ class Sndfile */ public static function ffi(): FFI { - return LibraryLoader::getInstance('sndfile'); + self::$loader ??= new LibraryLoader; + return self::$loader->get('sndfile'); } /** diff --git a/src/WhisperContext.php b/src/WhisperContext.php index b612ff2..e9fd828 100644 --- a/src/WhisperContext.php +++ b/src/WhisperContext.php @@ -16,14 +16,15 @@ class WhisperContext /** * Create a new WhisperContext from a file, with parameters. * - * @param string $modelPath The path to the model file. - * @param WhisperContextParameters|null $params A parameter struct containing the parameters to use. + * @param string $modelPath The path to the model file. + * @param WhisperContextParameters|null $params A parameter struct containing the parameters to use. * * @throws WhisperException */ public function __construct(string $modelPath, ?WhisperContextParameters $params = null) { - $this->ffi = LibraryLoader::getInstance('whisper'); + $libraryLoader = new LibraryLoader(); + $this->ffi = $libraryLoader->get('whisper'); $this->setupLoggerCallback(); @@ -51,8 +52,8 @@ public function createState(): WhisperState /** * Convert the provided text into tokens. * - * @param string $text The text to convert. - * @param int $maxTokens The maximum number of tokens to return. + * @param string $text The text to convert. + * @param int $maxTokens The maximum number of tokens to return. */ public function tokenize(string $text, int $maxTokens): array { @@ -109,7 +110,7 @@ public function nAudioCtx(): int */ public function isMultilingual(): bool { - return (bool) $this->ffi->whisper_is_multilingual($this->ctx); + return (bool)$this->ffi->whisper_is_multilingual($this->ctx); } /** @@ -178,7 +179,7 @@ public function modelType(): int /** * Convert a token ID to a string. * - * @param int $tokenId The ID of the token to convert. + * @param int $tokenId The ID of the token to convert. */ public function tokenToStr(int $tokenId): string { @@ -262,7 +263,7 @@ public function tokenBeg(): int /** * Get the ID of a specified language token * - * @param int $langId The ID of the language + * @param int $langId The ID of the language */ public function tokenLang(int $langId): int { @@ -272,7 +273,7 @@ public function tokenLang(int $langId): int /** * Return the id of the specified language, returns -1 if not found * - * @param string $lang The language to get the ID of + * @param string $lang The language to get the ID of */ public function langId(string $lang): int { @@ -292,7 +293,7 @@ public function langId(string $lang): int /** * Return the short string of the specified language id (e.g. 2 -> "de"), returns nullptr if not found * - * @param int $langId The ID of the language + * @param int $langId The ID of the language */ public function langStr(int $langId): string { @@ -302,7 +303,7 @@ public function langStr(int $langId): string /** * Return the short string of the specified language name (e.g. 2 -> "german"), returns nullptr if not found * - * @param int $langId The ID of the language + * @param int $langId The ID of the language */ public function langStrFull(int $langId): string { @@ -354,7 +355,7 @@ public function nSegments(): int /** * Get the text of the segment at the specified index. * - * @param int $index Segment index. + * @param int $index Segment index. */ public function getSegmentText(int $index): string { @@ -364,7 +365,7 @@ public function getSegmentText(int $index): string /** * Get the start time of the segment at the specified index. * - * @param int $index Segment index. + * @param int $index Segment index. */ public function getSegmentStartTime(int $index): int { @@ -374,7 +375,7 @@ public function getSegmentStartTime(int $index): int /** * Get the end time of the segment at the specified index. * - * @param int $index Segment index. + * @param int $index Segment index. */ public function getSegmentEndTime(int $index): int { @@ -384,7 +385,7 @@ public function getSegmentEndTime(int $index): int /** * Get number of tokens in the specified segment. * - * @param int $index Segment index. + * @param int $index Segment index. */ public function nTokens(int $index): int { @@ -394,8 +395,8 @@ public function nTokens(int $index): int /** * Get the token text of the specified token in the specified segment. * - * @param int $index Segment index. - * @param int $token Token index. + * @param int $index Segment index. + * @param int $token Token index. */ public function tokenText(int $index, int $token): string { @@ -416,8 +417,8 @@ public function tokenData(int $index, int $token): ?TokenData /** * Get the token ID of the specified token in the specified segment. * - * @param int $index Segment index. - * @param int $token Token index. + * @param int $index Segment index. + * @param int $token Token index. */ public function tokenId(int $index, int $token): int { @@ -427,8 +428,8 @@ public function tokenId(int $index, int $token): int /** * Get the probability of the specified token in the specified segment. * - * @param int $index Segment index. - * @param int $token Token index. + * @param int $index Segment index. + * @param int $token Token index. */ public function tokenProb(int $index, int $token): float { diff --git a/tests/Unit/WhisperContextParametersTest.php b/tests/Unit/WhisperContextParametersTest.php index bb01e06..b60ce63 100644 --- a/tests/Unit/WhisperContextParametersTest.php +++ b/tests/Unit/WhisperContextParametersTest.php @@ -8,7 +8,7 @@ use Codewithkyrian\Whisper\WhisperContextParameters; beforeEach(function () { - $this->ffi = LibraryLoader::getInstance('whisper'); + $this->ffi = (new LibraryLoader())->get('whisper'); }); it('correctly converts default parameters to C structure', function () { diff --git a/tests/Unit/WhisperFullParamsTest.php b/tests/Unit/WhisperFullParamsTest.php index 3df9b40..d6fa283 100644 --- a/tests/Unit/WhisperFullParamsTest.php +++ b/tests/Unit/WhisperFullParamsTest.php @@ -8,7 +8,7 @@ use Codewithkyrian\Whisper\WhisperGrammarElementType; beforeEach(function () { - $this->ffi = LibraryLoader::getInstance('whisper'); + $this->ffi = (new LibraryLoader())->get('whisper'); }); it('correctly converts default parameters to C structure', function () {