forked from olilarkin/skia-builder
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathbuild-whispercpp.py
More file actions
298 lines (236 loc) · 9.61 KB
/
Copy pathbuild-whispercpp.py
File metadata and controls
298 lines (236 loc) · 9.61 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
#!/usr/bin/env python3
"""
Build whisper.cpp static library for multiple platforms.
whisper.cpp provides speech-to-text inference in C/C++. Used by PixieAI
for local speech recognition and transcription.
Source: https://github.com/ggerganov/whisper.cpp
"""
import argparse
import os
import shutil
import subprocess
import sys
import urllib.request
import zipfile
from pathlib import Path
WHISPERCPP_VERSION = "v1.7.5"
WHISPERCPP_URL = f"https://github.com/ggerganov/whisper.cpp/archive/refs/tags/{WHISPERCPP_VERSION}.zip"
def parse_args():
parser = argparse.ArgumentParser(description="Build whisper.cpp static library")
parser.add_argument("platform", choices=["mac", "ios", "android", "linux", "win"],
help="Target platform")
parser.add_argument("-archs", help="Target architectures (comma separated)", default=None)
parser.add_argument("-config", choices=["Release", "Debug"], default="Release")
parser.add_argument("-out", help="Output directory", default="build")
parser.add_argument("-version", help="whisper.cpp version/tag", default=WHISPERCPP_VERSION)
parser.add_argument("-ndk", help="Android NDK path", default=None)
return parser.parse_args()
def run_command(cmd, cwd=None, env=None, shell=False):
print(f"Running: {cmd if shell else ' '.join(cmd)}")
try:
subprocess.check_call(cmd, cwd=cwd, env=env, shell=shell)
except subprocess.CalledProcessError as e:
print(f"Command failed with exit code {e.returncode}")
sys.exit(1)
def download_source(version, dest_dir):
"""Download and extract whisper.cpp source."""
# Strip 'v' prefix for directory name but keep for URL
dir_version = version.lstrip("v")
url = f"https://github.com/ggerganov/whisper.cpp/archive/refs/tags/{version}.zip"
zip_path = dest_dir / f"whisper.cpp-{dir_version}.zip"
extract_dir = dest_dir / f"whisper.cpp-{dir_version}"
if extract_dir.exists():
print(f"whisper.cpp source already exists at {extract_dir}")
return extract_dir
dest_dir.mkdir(parents=True, exist_ok=True)
print(f"Downloading whisper.cpp {version}...")
urllib.request.urlretrieve(url, zip_path)
print(f"Extracting to {dest_dir}...")
with zipfile.ZipFile(zip_path, 'r') as zip_ref:
zip_ref.extractall(dest_dir)
zip_path.unlink()
return extract_dir
def get_default_arch(platform):
"""Return default architecture for a given platform."""
defaults = {
"mac": "arm64",
"ios": "arm64",
"android": "arm64",
"linux": "x64",
"win": "x64",
}
return defaults[platform]
def get_ndk_path(args_ndk):
"""Resolve Android NDK path."""
if args_ndk:
return args_ndk
for env_var in ("ANDROID_NDK_HOME", "ANDROID_NDK_ROOT", "ANDROID_NDK"):
val = os.environ.get(env_var)
if val:
return val
print("Error: Android NDK not found. Set ANDROID_NDK_HOME or use -ndk flag.")
sys.exit(1)
def get_cmake_flags(platform, arch, config, ndk_path=None):
"""Get CMake configure flags for the target."""
flags = [
f"-DCMAKE_BUILD_TYPE={config}",
"-DBUILD_SHARED_LIBS=OFF",
"-DWHISPER_BUILD_TESTS=OFF",
"-DWHISPER_BUILD_EXAMPLES=OFF",
"-DWHISPER_BUILD_SERVER=OFF",
]
if platform == "mac":
if arch == "arm64":
flags.append("-DCMAKE_OSX_ARCHITECTURES=arm64")
elif arch in ("x64", "x86_64"):
flags.append("-DCMAKE_OSX_ARCHITECTURES=x86_64")
flags.append("-DCMAKE_OSX_DEPLOYMENT_TARGET=13.0")
# Disable native CPU detection to avoid i8mm intrinsics on CI runners
flags.append("-DGGML_NATIVE=OFF")
# Enable Metal + CoreML on macOS
flags.append("-DGGML_METAL=ON")
flags.append("-DGGML_METAL_EMBED_LIBRARY=ON")
flags.append("-DWHISPER_COREML=OFF") # CoreML adds complexity, start without it
flags.extend(["-G", "Ninja"])
flags.append("-DCMAKE_POSITION_INDEPENDENT_CODE=ON")
elif platform == "ios":
flags.append("-DCMAKE_SYSTEM_NAME=iOS")
flags.append("-DCMAKE_OSX_DEPLOYMENT_TARGET=16.0")
if arch == "arm64":
flags.append("-DCMAKE_OSX_ARCHITECTURES=arm64")
flags.append("-DGGML_METAL=ON")
flags.append("-DGGML_METAL_EMBED_LIBRARY=ON")
flags.append("-DWHISPER_COREML=OFF")
flags.append("-DGGML_OPENMP=OFF")
flags.extend(["-G", "Ninja"])
flags.append("-DCMAKE_POSITION_INDEPENDENT_CODE=ON")
elif platform == "android":
ndk = get_ndk_path(ndk_path)
toolchain = os.path.join(ndk, "build", "cmake", "android.toolchain.cmake")
flags.append(f"-DCMAKE_TOOLCHAIN_FILE={toolchain}")
flags.append("-DANDROID_PLATFORM=android-28")
abi_map = {
"arm64": "arm64-v8a",
"arm": "armeabi-v7a",
"x64": "x86_64",
"x86": "x86",
}
abi = abi_map.get(arch, "arm64-v8a")
flags.append(f"-DANDROID_ABI={abi}")
flags.append("-DGGML_OPENMP=OFF")
flags.extend(["-G", "Ninja"])
elif platform == "linux":
flags.extend(["-G", "Ninja"])
flags.append("-DCMAKE_POSITION_INDEPENDENT_CODE=ON")
elif platform == "win":
if arch in ("x64", "x86_64"):
flags.extend(["-A", "x64"])
elif arch == "x86":
flags.extend(["-A", "Win32"])
flags.append("-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded")
flags.append("-DCMAKE_POLICY_DEFAULT_CMP0091=NEW")
return flags
def build_whispercpp(source_dir, build_dir, platform, arch, config, ndk_path=None):
"""Build whisper.cpp using CMake."""
cmake_build_dir = build_dir / f"cmake-build-whispercpp-{platform}-{arch}"
cmake_build_dir.mkdir(parents=True, exist_ok=True)
cmake_args = ["cmake", str(source_dir)]
cmake_args.extend(get_cmake_flags(platform, arch, config, ndk_path))
# Configure
run_command(cmake_args, cwd=cmake_build_dir)
# Build
build_cmd = ["cmake", "--build", ".", "--config", config]
if platform != "win":
build_cmd.extend(["--parallel"])
run_command(build_cmd, cwd=cmake_build_dir)
return cmake_build_dir
def find_libraries(cmake_build_dir, platform):
"""Find all built static libraries."""
ext = "*.lib" if platform == "win" else "*.a"
libs = {}
# Key libraries we want from whisper.cpp
wanted = ["whisper", "ggml"]
for f in cmake_build_dir.rglob(ext):
name = f.stem.lower()
if name.startswith("lib"):
name = name[3:]
for w in wanted:
if w in name:
libs[f.name] = f
break
return libs
def copy_outputs(cmake_build_dir, output_dir, platform, arch, config):
"""Copy built libraries to output directory."""
libs = find_libraries(cmake_build_dir, platform)
if not libs:
print(f"Error: No libraries found in {cmake_build_dir}")
ext = "*.lib" if platform == "win" else "*.a"
for f in cmake_build_dir.rglob(ext):
print(f" Found: {f}")
sys.exit(1)
lib_dir = output_dir / f"whispercpp-{platform}" / "lib"
if platform in ("mac", "ios"):
lib_dir = lib_dir / arch
lib_dir.mkdir(parents=True, exist_ok=True)
for lib_name, lib_path in libs.items():
dest = lib_dir / lib_name
print(f"Copying {lib_path} -> {dest}")
shutil.copy2(lib_path, dest)
return lib_dir
def copy_headers(source_dir, output_dir):
"""Copy whisper.cpp public headers."""
include_dest = output_dir / "include" / "whispercpp"
include_dest.mkdir(parents=True, exist_ok=True)
# Main public header
public_headers = [
"include/whisper.h",
]
for header_rel in public_headers:
header = source_dir / header_rel
if header.exists():
dest = include_dest / header.name
print(f"Copying header: {header.name}")
shutil.copy2(header, dest)
# ggml headers (whisper.cpp bundles its own ggml)
ggml_include = source_dir / "ggml" / "include"
if ggml_include.exists():
ggml_dest = include_dest / "ggml"
ggml_dest.mkdir(parents=True, exist_ok=True)
for header in ggml_include.glob("*.h"):
dest = ggml_dest / header.name
print(f"Copying header: ggml/{header.name}")
shutil.copy2(header, dest)
# Also check top-level include
top_include = source_dir / "include"
if top_include.exists():
for header in top_include.glob("ggml*.h"):
dest = include_dest / "ggml" / header.name
(include_dest / "ggml").mkdir(parents=True, exist_ok=True)
print(f"Copying header: ggml/{header.name}")
shutil.copy2(header, dest)
def main():
args = parse_args()
root_dir = Path(__file__).parent.absolute()
third_party_dir = root_dir / "third_party"
build_dir = Path(args.out).absolute()
# Download source
source_dir = download_source(args.version, third_party_dir)
arch = args.archs or get_default_arch(args.platform)
archs = [a.strip() for a in arch.split(",")]
for arch in archs:
print(f"\n{'='*60}")
print(f"Building whisper.cpp for {args.platform} {arch} ({args.config})")
print(f"{'='*60}\n")
cmake_build_dir = build_whispercpp(
source_dir, build_dir, args.platform, arch, args.config,
ndk_path=args.ndk,
)
copy_outputs(cmake_build_dir, build_dir, args.platform, arch, args.config)
# Copy headers once
copy_headers(source_dir, build_dir)
print(f"\n{'='*60}")
print("Build complete!")
print(f"Output: {build_dir}")
print(f"{'='*60}\n")
if __name__ == "__main__":
main()