Skip to content

Commit 18f5b12

Browse files
feat: Add Windows support (#5)
* feat: Add windows x64 support * feat: Include windows in CI tests * ci: Run tests only on windows * fix: Pass lib directory without realpath as it isn't assured to exist * ci: Restore tests on all platforms
1 parent 81bfa53 commit 18f5b12

8 files changed

Lines changed: 91 additions & 53 deletions

File tree

.github/workflows/tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ jobs:
99
strategy:
1010
fail-fast: false
1111
matrix:
12-
os: [macos-latest, macos-13, ubuntu-latest]
12+
os: [ubuntu-latest, macos-latest, windows-latest]
1313
php: [8.1, 8.2, 8.3, 8.4]
1414

1515
name: Tests PHP${{ matrix.php }} - ${{ matrix.os }}

src/LibraryLoader.php

Lines changed: 54 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -24,62 +24,59 @@ class LibraryLoader
2424
'lib_prefix' => 'libsamplerate',
2525
],
2626
];
27-
2827
private const WHISPER_CPP_VERSION = '1.7.2';
2928
private const DOWNLOAD_URL = 'https://huggingface.co/codewithkyrian/whisper.php/resolve/%s/libs/%s.zip';
3029

3130
private static array $instances = [];
31+
private ?PlatformDetector $platformDetector;
32+
private ?FFI $kernel32 = null;
3233

33-
private static ?PlatformDetector $platformDetector = null;
34+
public function __construct(?PlatformDetector $platformDetector = null)
35+
{
36+
$this->platformDetector = $platformDetector ?? new PlatformDetector();
37+
$this->addDllDirectory();
38+
}
39+
40+
public function __destruct()
41+
{
42+
$this->resetDllDirectory();
43+
}
3444

3545
/**
3646
* Gets the FFI instance for the specified library
3747
*/
38-
public static function getInstance(string $library): FFI
48+
public function get(string $library): FFI
3949
{
4050
if (!isset(self::$instances[$library])) {
41-
self::$instances[$library] = self::createFFIInstance($library);
51+
self::$instances[$library] = $this->load($library);
4252
}
4353

4454
return self::$instances[$library];
4555
}
4656

4757
/**
48-
* Creates a new FFI instance for the specified library
58+
* Loads a new FFI instance for the specified library
4959
*/
50-
private static function createFFIInstance(string $library): FFI
60+
private function load(string $library): FFI
5161
{
5262
if (!isset(self::LIBRARY_CONFIGS[$library])) {
5363
throw new RuntimeException("Unsupported library: {$library}");
5464
}
5565

5666
$config = self::LIBRARY_CONFIGS[$library];
57-
$detector = self::getPlatformDetector();
5867

5968
$headerPath = self::getHeaderPath($config['header']);
6069
$libPath = self::getLibraryPath(
6170
$config['lib_prefix'],
62-
$detector->getLibraryExtension(),
63-
$detector->getPlatformIdentifier()
71+
$this->platformDetector->getLibraryExtension(),
72+
$this->platformDetector->getPlatformIdentifier()
6473
);
6574

6675
if (!file_exists($libPath)) {
67-
self::downloadLibraries();
68-
}
69-
70-
return FFI::cdef(
71-
file_get_contents($headerPath),
72-
$libPath
73-
);
74-
}
75-
76-
private static function getPlatformDetector(): PlatformDetector
77-
{
78-
if (self::$platformDetector === null) {
79-
self::$platformDetector = new PlatformDetector;
76+
$this->downloadLibraries();
8077
}
8178

82-
return self::$platformDetector;
79+
return FFI::cdef(file_get_contents($headerPath), $libPath);
8380
}
8481

8582
private static function getHeaderPath(string $headerFile): string
@@ -89,16 +86,20 @@ private static function getHeaderPath(string $headerFile): string
8986

9087
private static function getLibraryPath(string $prefix, string $extension, string $platform): string
9188
{
92-
return self::joinPaths(dirname(__DIR__), 'lib', $platform, "$prefix.$extension");
89+
return self::joinPaths(self::getLibraryDirectory($platform), "$prefix.$extension");
90+
}
91+
92+
private static function getLibraryDirectory(string $platform): string
93+
{
94+
return self::joinPaths(dirname(__DIR__), 'lib', $platform);
9395
}
9496

9597
/**
9698
* Download libraries from Hugging Face
9799
*/
98-
private static function downloadLibraries(): void
100+
private function downloadLibraries(): void
99101
{
100-
$detector = self::getPlatformDetector();
101-
$platform = $detector->getPlatformIdentifier();
102+
$platform = $this->platformDetector->getPlatformIdentifier();
102103

103104
$url = sprintf(self::DOWNLOAD_URL, self::WHISPER_CPP_VERSION, $platform);
104105

@@ -136,6 +137,32 @@ private static function downloadLibraries(): void
136137
}
137138
}
138139

140+
/**
141+
* Add DLL directory to search path for Windows
142+
*/
143+
private function addDllDirectory(): void
144+
{
145+
if (!$this->platformDetector->isWindows()) return;
146+
147+
$libDir = ($this->getLibraryDirectory($this->platformDetector->getPlatformIdentifier()));
148+
$this->kernel32 ??= FFI::cdef("
149+
int SetDllDirectoryA(const char* lpPathName);
150+
int SetDefaultDllDirectories(unsigned long DirectoryFlags);
151+
", 'kernel32.dll');
152+
153+
$this->kernel32->SetDllDirectoryA($libDir);
154+
}
155+
156+
/**
157+
* Reset DLL directory search path
158+
*/
159+
private function resetDllDirectory(): void
160+
{
161+
if ($this->kernel32 !== null) {
162+
$this->kernel32->SetDllDirectoryA(null);
163+
}
164+
}
165+
139166
private static function joinPaths(string ...$args): string
140167
{
141168
$paths = [];

src/PlatformDetector.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,9 @@ private function isPlatformSupported(): bool
5959
{
6060
return isset(self::SUPPORTED_PLATFORMS[$this->os][$this->arch]);
6161
}
62+
63+
public function isWindows(): bool
64+
{
65+
return $this->os === 'windows';
66+
}
6267
}

src/Samplerate.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
class Samplerate
1414
{
15+
private static ?LibraryLoader $loader = null;
16+
1517
/**
1618
* Returns an instance of the FFI class after checking if it has already been instantiated.
1719
* If not, it creates a new instance by defining the header contents and library path.
@@ -22,7 +24,8 @@ class Samplerate
2224
*/
2325
public static function ffi(): FFI
2426
{
25-
return LibraryLoader::getInstance('samplerate');
27+
self::$loader ??= new LibraryLoader;
28+
return self::$loader->get('samplerate');
2629
}
2730

2831
/**

src/Sndfile.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
class Sndfile
1414
{
15+
private static ?LibraryLoader $loader = null;
1516
/**
1617
* Returns an instance of the FFI class after checking if it has already been instantiated.
1718
* If not, it creates a new instance by defining the header contents and library path.
@@ -22,7 +23,8 @@ class Sndfile
2223
*/
2324
public static function ffi(): FFI
2425
{
25-
return LibraryLoader::getInstance('sndfile');
26+
self::$loader ??= new LibraryLoader;
27+
return self::$loader->get('sndfile');
2628
}
2729

2830
/**

src/WhisperContext.php

Lines changed: 22 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,15 @@ class WhisperContext
1616
/**
1717
* Create a new WhisperContext from a file, with parameters.
1818
*
19-
* @param string $modelPath The path to the model file.
20-
* @param WhisperContextParameters|null $params A parameter struct containing the parameters to use.
19+
* @param string $modelPath The path to the model file.
20+
* @param WhisperContextParameters|null $params A parameter struct containing the parameters to use.
2121
*
2222
* @throws WhisperException
2323
*/
2424
public function __construct(string $modelPath, ?WhisperContextParameters $params = null)
2525
{
26-
$this->ffi = LibraryLoader::getInstance('whisper');
26+
$libraryLoader = new LibraryLoader();
27+
$this->ffi = $libraryLoader->get('whisper');
2728

2829
$this->setupLoggerCallback();
2930

@@ -51,8 +52,8 @@ public function createState(): WhisperState
5152
/**
5253
* Convert the provided text into tokens.
5354
*
54-
* @param string $text The text to convert.
55-
* @param int $maxTokens The maximum number of tokens to return.
55+
* @param string $text The text to convert.
56+
* @param int $maxTokens The maximum number of tokens to return.
5657
*/
5758
public function tokenize(string $text, int $maxTokens): array
5859
{
@@ -109,7 +110,7 @@ public function nAudioCtx(): int
109110
*/
110111
public function isMultilingual(): bool
111112
{
112-
return (bool) $this->ffi->whisper_is_multilingual($this->ctx);
113+
return (bool)$this->ffi->whisper_is_multilingual($this->ctx);
113114
}
114115

115116
/**
@@ -178,7 +179,7 @@ public function modelType(): int
178179
/**
179180
* Convert a token ID to a string.
180181
*
181-
* @param int $tokenId The ID of the token to convert.
182+
* @param int $tokenId The ID of the token to convert.
182183
*/
183184
public function tokenToStr(int $tokenId): string
184185
{
@@ -262,7 +263,7 @@ public function tokenBeg(): int
262263
/**
263264
* Get the ID of a specified language token
264265
*
265-
* @param int $langId The ID of the language
266+
* @param int $langId The ID of the language
266267
*/
267268
public function tokenLang(int $langId): int
268269
{
@@ -272,7 +273,7 @@ public function tokenLang(int $langId): int
272273
/**
273274
* Return the id of the specified language, returns -1 if not found
274275
*
275-
* @param string $lang The language to get the ID of
276+
* @param string $lang The language to get the ID of
276277
*/
277278
public function langId(string $lang): int
278279
{
@@ -292,7 +293,7 @@ public function langId(string $lang): int
292293
/**
293294
* Return the short string of the specified language id (e.g. 2 -> "de"), returns nullptr if not found
294295
*
295-
* @param int $langId The ID of the language
296+
* @param int $langId The ID of the language
296297
*/
297298
public function langStr(int $langId): string
298299
{
@@ -302,7 +303,7 @@ public function langStr(int $langId): string
302303
/**
303304
* Return the short string of the specified language name (e.g. 2 -> "german"), returns nullptr if not found
304305
*
305-
* @param int $langId The ID of the language
306+
* @param int $langId The ID of the language
306307
*/
307308
public function langStrFull(int $langId): string
308309
{
@@ -354,7 +355,7 @@ public function nSegments(): int
354355
/**
355356
* Get the text of the segment at the specified index.
356357
*
357-
* @param int $index Segment index.
358+
* @param int $index Segment index.
358359
*/
359360
public function getSegmentText(int $index): string
360361
{
@@ -364,7 +365,7 @@ public function getSegmentText(int $index): string
364365
/**
365366
* Get the start time of the segment at the specified index.
366367
*
367-
* @param int $index Segment index.
368+
* @param int $index Segment index.
368369
*/
369370
public function getSegmentStartTime(int $index): int
370371
{
@@ -374,7 +375,7 @@ public function getSegmentStartTime(int $index): int
374375
/**
375376
* Get the end time of the segment at the specified index.
376377
*
377-
* @param int $index Segment index.
378+
* @param int $index Segment index.
378379
*/
379380
public function getSegmentEndTime(int $index): int
380381
{
@@ -384,7 +385,7 @@ public function getSegmentEndTime(int $index): int
384385
/**
385386
* Get number of tokens in the specified segment.
386387
*
387-
* @param int $index Segment index.
388+
* @param int $index Segment index.
388389
*/
389390
public function nTokens(int $index): int
390391
{
@@ -394,8 +395,8 @@ public function nTokens(int $index): int
394395
/**
395396
* Get the token text of the specified token in the specified segment.
396397
*
397-
* @param int $index Segment index.
398-
* @param int $token Token index.
398+
* @param int $index Segment index.
399+
* @param int $token Token index.
399400
*/
400401
public function tokenText(int $index, int $token): string
401402
{
@@ -416,8 +417,8 @@ public function tokenData(int $index, int $token): ?TokenData
416417
/**
417418
* Get the token ID of the specified token in the specified segment.
418419
*
419-
* @param int $index Segment index.
420-
* @param int $token Token index.
420+
* @param int $index Segment index.
421+
* @param int $token Token index.
421422
*/
422423
public function tokenId(int $index, int $token): int
423424
{
@@ -427,8 +428,8 @@ public function tokenId(int $index, int $token): int
427428
/**
428429
* Get the probability of the specified token in the specified segment.
429430
*
430-
* @param int $index Segment index.
431-
* @param int $token Token index.
431+
* @param int $index Segment index.
432+
* @param int $token Token index.
432433
*/
433434
public function tokenProb(int $index, int $token): float
434435
{

tests/Unit/WhisperContextParametersTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use Codewithkyrian\Whisper\WhisperContextParameters;
99

1010
beforeEach(function () {
11-
$this->ffi = LibraryLoader::getInstance('whisper');
11+
$this->ffi = (new LibraryLoader())->get('whisper');
1212
});
1313

1414
it('correctly converts default parameters to C structure', function () {

tests/Unit/WhisperFullParamsTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
use Codewithkyrian\Whisper\WhisperGrammarElementType;
99

1010
beforeEach(function () {
11-
$this->ffi = LibraryLoader::getInstance('whisper');
11+
$this->ffi = (new LibraryLoader())->get('whisper');
1212
});
1313

1414
it('correctly converts default parameters to C structure', function () {

0 commit comments

Comments
 (0)