Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
81 changes: 54 additions & 27 deletions src/LibraryLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);

Expand Down Expand Up @@ -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 = [];
Expand Down
5 changes: 5 additions & 0 deletions src/PlatformDetector.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
}
}
5 changes: 4 additions & 1 deletion src/Samplerate.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -22,7 +24,8 @@ class Samplerate
*/
public static function ffi(): FFI
{
return LibraryLoader::getInstance('samplerate');
self::$loader ??= new LibraryLoader;
return self::$loader->get('samplerate');
}

/**
Expand Down
4 changes: 3 additions & 1 deletion src/Sndfile.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -22,7 +23,8 @@ class Sndfile
*/
public static function ffi(): FFI
{
return LibraryLoader::getInstance('sndfile');
self::$loader ??= new LibraryLoader;
return self::$loader->get('sndfile');
}

/**
Expand Down
43 changes: 22 additions & 21 deletions src/WhisperContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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);
}

/**
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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
{
Expand All @@ -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
{
Expand All @@ -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
{
Expand All @@ -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
{
Expand Down Expand Up @@ -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
{
Expand All @@ -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
{
Expand All @@ -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
{
Expand All @@ -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
{
Expand All @@ -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
{
Expand All @@ -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
{
Expand All @@ -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
{
Expand Down
2 changes: 1 addition & 1 deletion tests/Unit/WhisperContextParametersTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
2 changes: 1 addition & 1 deletion tests/Unit/WhisperFullParamsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down
Loading