hf2ollama-python-cli-tool 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hf2ollama/__init__.py +3 -0
- hf2ollama/cli.py +329 -0
- hf2ollama/core/__init__.py +1 -0
- hf2ollama/core/config.py +73 -0
- hf2ollama/core/ollama.py +137 -0
- hf2ollama/hf/__init__.py +1 -0
- hf2ollama/hf/alt_download.py +78 -0
- hf2ollama/hf/api.py +197 -0
- hf2ollama/hf/download.py +121 -0
- hf2ollama/modelfile/__init__.py +1 -0
- hf2ollama/modelfile/generator.py +90 -0
- hf2ollama/modelfile/templates.py +134 -0
- hf2ollama/network/__init__.py +1 -0
- hf2ollama/network/detect.py +58 -0
- hf2ollama/network/ssl_fix.py +76 -0
- hf2ollama/scripts/HfDownload.ps1 +221 -0
- hf2ollama/utils/__init__.py +1 -0
- hf2ollama/utils/vram.py +65 -0
- hf2ollama_python_cli_tool-1.0.0.data/data/hf2ollama/scripts/HfDownload.ps1 +221 -0
- hf2ollama_python_cli_tool-1.0.0.dist-info/METADATA +200 -0
- hf2ollama_python_cli_tool-1.0.0.dist-info/RECORD +24 -0
- hf2ollama_python_cli_tool-1.0.0.dist-info/WHEEL +4 -0
- hf2ollama_python_cli_tool-1.0.0.dist-info/entry_points.txt +2 -0
- hf2ollama_python_cli_tool-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Downloads a GGUF model file from HuggingFace using direct IP connection.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Alternative download method that connects to HuggingFace CDN servers
|
|
7
|
+
directly via IP address using PowerShell 7's .NET SslStream.
|
|
8
|
+
Useful when standard HTTPS downloads fail due to SSL or routing issues.
|
|
9
|
+
|
|
10
|
+
Requires PowerShell 7 (pwsh) and an elevated terminal.
|
|
11
|
+
|
|
12
|
+
.PARAMETER Repo
|
|
13
|
+
HuggingFace repo (e.g., "bartowski/Qwen2.5-Coder-7B-Instruct-GGUF").
|
|
14
|
+
|
|
15
|
+
.PARAMETER File
|
|
16
|
+
Exact GGUF filename. If omitted, lists available files.
|
|
17
|
+
|
|
18
|
+
.PARAMETER OutDir
|
|
19
|
+
Output directory. Defaults to script's directory.
|
|
20
|
+
|
|
21
|
+
.EXAMPLE
|
|
22
|
+
pwsh -ExecutionPolicy Bypass -File HfDownload.ps1 "bartowski/Qwen2.5-Coder-7B-Instruct-GGUF" "Qwen2.5-Coder-7B-Instruct-Q4_K_M.gguf"
|
|
23
|
+
#>
|
|
24
|
+
param(
|
|
25
|
+
[Parameter(Mandatory, Position=0)]
|
|
26
|
+
[string]$Repo,
|
|
27
|
+
|
|
28
|
+
[Parameter(Position=1)]
|
|
29
|
+
[string]$File,
|
|
30
|
+
|
|
31
|
+
[string]$OutDir
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
$ErrorActionPreference = 'Stop'
|
|
35
|
+
if (-not $OutDir) { $OutDir = $PSScriptRoot }
|
|
36
|
+
|
|
37
|
+
$US_EAST_IPS = @(
|
|
38
|
+
"35.173.17.142", "34.231.87.187", "44.217.206.136",
|
|
39
|
+
"3.216.102.62", "34.230.200.86", "100.50.185.240",
|
|
40
|
+
"100.57.83.12", "98.90.123.60", "100.31.16.245", "100.29.213.216"
|
|
41
|
+
)
|
|
42
|
+
$RouteExe = "$env:SystemRoot\System32\route.exe"
|
|
43
|
+
|
|
44
|
+
# --- Find Wi-Fi gateway ---
|
|
45
|
+
|
|
46
|
+
function Find-WifiGateway {
|
|
47
|
+
$output = & $RouteExe print -4 0.0.0.0 2>&1 | Out-String
|
|
48
|
+
$routes = @()
|
|
49
|
+
foreach ($line in $output -split "`n") {
|
|
50
|
+
$parts = $line.Trim() -split '\s+'
|
|
51
|
+
if ($parts.Count -ge 5 -and $parts[0] -eq '0.0.0.0' -and $parts[1] -eq '0.0.0.0' -and $parts[2] -ne 'On-link') {
|
|
52
|
+
$routes += @{ Gateway = $parts[2]; Metric = [int]$parts[4] }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if ($routes.Count -eq 0) { return $null }
|
|
56
|
+
$routes | Sort-Object { $_.Metric } -Descending | Select-Object -First 1 -ExpandProperty Gateway
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function Find-WifiIfIndex {
|
|
60
|
+
$ipconfig = & "$env:SystemRoot\System32\ipconfig.exe" 2>&1 | Out-String
|
|
61
|
+
$inWifi = $false
|
|
62
|
+
foreach ($line in $ipconfig -split "`n") {
|
|
63
|
+
if ($line -match 'Wi-Fi|Wireless') { $inWifi = $true }
|
|
64
|
+
elseif ($inWifi -and $line -match '%(\d+)') { return [int]$Matches[1] }
|
|
65
|
+
elseif ($inWifi -and $line.Trim() -and -not $line.StartsWith(' ')) { $inWifi = $false }
|
|
66
|
+
}
|
|
67
|
+
return 0
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# --- Route management ---
|
|
71
|
+
|
|
72
|
+
$script:routes = @()
|
|
73
|
+
|
|
74
|
+
function Add-Route([string]$IP, [string]$Gateway, [int]$IfIdx) {
|
|
75
|
+
$args2 = "add $IP mask 255.255.255.255 $Gateway"
|
|
76
|
+
if ($IfIdx -gt 0) { $args2 += " IF $IfIdx" }
|
|
77
|
+
$args2 += " metric 1"
|
|
78
|
+
& $RouteExe $args2.Split(' ') 2>&1 | Out-Null
|
|
79
|
+
$script:routes += $IP
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function Remove-AllRoutes {
|
|
83
|
+
foreach ($ip in $script:routes) { & $RouteExe delete $ip 2>&1 | Out-Null }
|
|
84
|
+
$script:routes = @()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# --- Main ---
|
|
88
|
+
|
|
89
|
+
Write-Host "`n[1/3] Setup..." -ForegroundColor Cyan
|
|
90
|
+
$gateway = Find-WifiGateway
|
|
91
|
+
if (-not $gateway) { Write-Error "Cannot find Wi-Fi gateway. Ensure Wi-Fi is connected." }
|
|
92
|
+
$ifIdx = Find-WifiIfIndex
|
|
93
|
+
Write-Host " Gateway: $gateway (IF $ifIdx)"
|
|
94
|
+
|
|
95
|
+
# List files if not specified
|
|
96
|
+
if (-not $File) {
|
|
97
|
+
Write-Host "`n Fetching files from huggingface.co/$Repo..." -ForegroundColor DarkGray
|
|
98
|
+
$response = Invoke-RestMethod -Uri "https://huggingface.co/api/models/$Repo/tree/main" -TimeoutSec 30
|
|
99
|
+
$ggufFiles = @($response | Where-Object { $_.path -like '*.gguf' -and $_.type -eq 'file' } |
|
|
100
|
+
Select-Object @{N='Name';E={$_.path}}, @{N='SizeGB';E={[math]::Round($_.size/1GB,2)}} | Sort-Object SizeGB)
|
|
101
|
+
if ($ggufFiles.Count -eq 0) { Write-Error "No GGUF files in $Repo" }
|
|
102
|
+
Write-Host "`n Available:" -ForegroundColor Yellow
|
|
103
|
+
for ($i=0; $i -lt $ggufFiles.Count; $i++) {
|
|
104
|
+
$f = $ggufFiles[$i]
|
|
105
|
+
$lbl = if ($f.Name -match 'Q4_K_M') {' (recommended)'} else {''}
|
|
106
|
+
Write-Host " [$($i+1)] $($f.Name) ($($f.SizeGB) GB)$lbl"
|
|
107
|
+
}
|
|
108
|
+
$choice = Read-Host "`n Select [1-$($ggufFiles.Count)]"
|
|
109
|
+
$File = $ggufFiles[[int]$choice - 1].Name
|
|
110
|
+
}
|
|
111
|
+
Write-Host " File: $File" -ForegroundColor Green
|
|
112
|
+
|
|
113
|
+
# Get redirect URL
|
|
114
|
+
Write-Host "`n[2/3] Getting download URL..." -ForegroundColor Cyan
|
|
115
|
+
$hfUrl = "https://huggingface.co/$Repo/resolve/main/$File"
|
|
116
|
+
$redirectUrl = $null
|
|
117
|
+
try { Invoke-WebRequest -Uri $hfUrl -Method HEAD -MaximumRedirection 0 -ErrorAction Stop } catch {
|
|
118
|
+
$redirectUrl = $_.Exception.Response.Headers.Location.ToString()
|
|
119
|
+
}
|
|
120
|
+
if (-not $redirectUrl) { Write-Error "Failed to get redirect URL from HuggingFace." }
|
|
121
|
+
$cdnUri = [System.Uri]$redirectUrl
|
|
122
|
+
Write-Host " CDN: $($cdnUri.Host)"
|
|
123
|
+
|
|
124
|
+
# Build candidate IPs
|
|
125
|
+
$candidates = @()
|
|
126
|
+
if ($cdnUri.PathAndQuery -match 'xet-bridge-us') {
|
|
127
|
+
$candidates += $US_EAST_IPS
|
|
128
|
+
Write-Host " US bridge detected; using US East IPs" -ForegroundColor DarkGray
|
|
129
|
+
}
|
|
130
|
+
$dnsIPs = @((Resolve-DnsName $cdnUri.Host -Type A -ErrorAction SilentlyContinue).IPAddress)
|
|
131
|
+
foreach ($ip in $dnsIPs) { if ($ip -notin $candidates) { $candidates += $ip } }
|
|
132
|
+
|
|
133
|
+
# Add routes
|
|
134
|
+
foreach ($ip in $candidates) { Add-Route $ip $gateway $ifIdx }
|
|
135
|
+
|
|
136
|
+
# Download
|
|
137
|
+
Write-Host "`n[3/3] Downloading..." -ForegroundColor Cyan
|
|
138
|
+
$destPath = Join-Path $OutDir $File
|
|
139
|
+
$success = $false
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
foreach ($ip in $candidates) {
|
|
143
|
+
try {
|
|
144
|
+
Write-Host " Trying $ip... " -NoNewline
|
|
145
|
+
|
|
146
|
+
$tcp = New-Object System.Net.Sockets.TcpClient
|
|
147
|
+
$tcp.ReceiveBufferSize = 8 * 1024 * 1024
|
|
148
|
+
$tcp.Connect($ip, 443)
|
|
149
|
+
|
|
150
|
+
$ssl = New-Object System.Net.Security.SslStream($tcp.GetStream(), $false, ({$true}))
|
|
151
|
+
$ssl.AuthenticateAsClient($ip)
|
|
152
|
+
|
|
153
|
+
# Verify the cert is from the expected CDN, not a proxy
|
|
154
|
+
$issuer = $ssl.RemoteCertificate.Issuer
|
|
155
|
+
if ($issuer -notmatch 'Amazon|DigiCert|Let''s Encrypt|Google Trust|Cloudflare') {
|
|
156
|
+
Write-Host "unexpected cert issuer" -ForegroundColor Yellow
|
|
157
|
+
$ssl.Close(); $tcp.Close(); continue
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
# Send GET
|
|
161
|
+
$req = "GET $($cdnUri.PathAndQuery) HTTP/1.1`r`nHost: $($cdnUri.Host)`r`nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)`r`nAccept: */*`r`nConnection: keep-alive`r`n`r`n"
|
|
162
|
+
$ssl.Write([System.Text.Encoding]::ASCII.GetBytes($req)); $ssl.Flush()
|
|
163
|
+
|
|
164
|
+
# Read headers
|
|
165
|
+
$headerBytes = New-Object System.Collections.Generic.List[byte]
|
|
166
|
+
$prev = [byte[]]::new(4)
|
|
167
|
+
while ($true) {
|
|
168
|
+
$b = $ssl.ReadByte(); if ($b -eq -1) { throw "Connection closed" }
|
|
169
|
+
$headerBytes.Add([byte]$b)
|
|
170
|
+
$prev[0]=$prev[1];$prev[1]=$prev[2];$prev[2]=$prev[3];$prev[3]=[byte]$b
|
|
171
|
+
if ($prev[0]-eq13 -and $prev[1]-eq10 -and $prev[2]-eq13 -and $prev[3]-eq10) { break }
|
|
172
|
+
}
|
|
173
|
+
$hdr = [System.Text.Encoding]::ASCII.GetString($headerBytes.ToArray())
|
|
174
|
+
$status = ($hdr -split "`r`n")[0]
|
|
175
|
+
|
|
176
|
+
if ($status -notmatch '200') {
|
|
177
|
+
Write-Host $status -ForegroundColor Yellow
|
|
178
|
+
$ssl.Close(); $tcp.Close(); continue
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
$contentLength = [long]0
|
|
182
|
+
if ($hdr -match 'content-length:\s*(\d+)') { $contentLength = [long]$Matches[1] }
|
|
183
|
+
Write-Host "200 OK ($([math]::Round($contentLength/1GB,2)) GB)" -ForegroundColor Green
|
|
184
|
+
Write-Host ""
|
|
185
|
+
|
|
186
|
+
# Stream to file
|
|
187
|
+
$fs = [System.IO.File]::Create($destPath)
|
|
188
|
+
$buf = New-Object byte[] (4*1024*1024)
|
|
189
|
+
$total = [long]0; $sw = [System.Diagnostics.Stopwatch]::StartNew(); $lr = [long]0
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
while ($total -lt $contentLength) {
|
|
193
|
+
$n = [int][math]::Min([long]$buf.Length, [long]($contentLength - $total))
|
|
194
|
+
$read = $ssl.Read($buf, 0, $n); if ($read -eq 0) { break }
|
|
195
|
+
$fs.Write($buf, 0, $read); $total += $read
|
|
196
|
+
if ($total - $lr -gt 100MB) {
|
|
197
|
+
$pct = [math]::Round($total*100/$contentLength,1)
|
|
198
|
+
$spd = if($sw.Elapsed.TotalSeconds -gt 0){[math]::Round($total/$sw.Elapsed.TotalSeconds/1MB,1)}else{0}
|
|
199
|
+
$eta = if($spd -gt 0){$r=($contentLength-$total)/($spd*1MB);if($r -gt 60){"$([math]::Round($r/60,1))m"}else{"$([math]::Round($r))s"}}else{'?'}
|
|
200
|
+
Write-Host " [$pct%] $([math]::Round($total/1GB,2))GB @ ${spd}MB/s ETA:$eta"
|
|
201
|
+
$lr = $total
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
Write-Host " [100%] Done! $([math]::Round($total/1GB,2)) GB in $([math]::Round($sw.Elapsed.TotalMinutes,1)) min" -ForegroundColor Green
|
|
205
|
+
} finally { $fs.Close(); $ssl.Close(); $tcp.Close() }
|
|
206
|
+
|
|
207
|
+
if ($total -ge $contentLength * 0.99) { $success = $true }
|
|
208
|
+
else { Write-Warning "Incomplete: $total / $contentLength bytes" }
|
|
209
|
+
break
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
Write-Host "FAILED ($($_.Exception.Message))" -ForegroundColor Red
|
|
213
|
+
continue
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} finally {
|
|
217
|
+
Remove-AllRoutes
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (-not $success) { Write-Error "All $($candidates.Count) CDN IPs failed." }
|
|
221
|
+
Write-Host "`n Saved: $destPath" -ForegroundColor Green
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Utility modules - VRAM detection and display formatting."""
|
hf2ollama/utils/vram.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""VRAM detection and quantization recommendation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
QUANT_MULTIPLIERS = {
|
|
10
|
+
"Q2_K": 0.31,
|
|
11
|
+
"Q3_K_M": 0.44,
|
|
12
|
+
"Q4_K_M": 0.56,
|
|
13
|
+
"Q5_K_M": 0.68,
|
|
14
|
+
"Q6_K": 0.81,
|
|
15
|
+
"Q8_0": 1.0,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class VRAMInfo:
|
|
21
|
+
"""GPU VRAM information from nvidia-smi."""
|
|
22
|
+
|
|
23
|
+
available_gb: float
|
|
24
|
+
gpu_name: str = "Unknown"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def detect_vram() -> VRAMInfo | None:
|
|
28
|
+
"""Detect available GPU VRAM using nvidia-smi."""
|
|
29
|
+
try:
|
|
30
|
+
result = subprocess.run(
|
|
31
|
+
["nvidia-smi", "--query-gpu=memory.total,name", "--format=csv,noheader,nounits"],
|
|
32
|
+
capture_output=True,
|
|
33
|
+
text=True,
|
|
34
|
+
timeout=10,
|
|
35
|
+
)
|
|
36
|
+
if result.returncode == 0:
|
|
37
|
+
line = result.stdout.strip().splitlines()[0]
|
|
38
|
+
parts = line.split(",")
|
|
39
|
+
vram_mb = float(parts[0].strip())
|
|
40
|
+
name = parts[1].strip() if len(parts) > 1 else "NVIDIA GPU"
|
|
41
|
+
return VRAMInfo(available_gb=vram_mb / 1024, gpu_name=name)
|
|
42
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def recommend_quant(model_params_b: float, vram_gb: float) -> list[str]:
|
|
49
|
+
"""Recommend quantizations that fit in the given VRAM.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
model_params_b: Model parameter count in billions (e.g., 7.0 for 7B).
|
|
53
|
+
vram_gb: Available VRAM in GB.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
List of quantization names that should fit, from smallest to largest.
|
|
57
|
+
|
|
58
|
+
"""
|
|
59
|
+
fits = []
|
|
60
|
+
for quant, multiplier in sorted(QUANT_MULTIPLIERS.items(), key=lambda x: x[1]):
|
|
61
|
+
estimated_gb = model_params_b * multiplier * 1.1
|
|
62
|
+
if estimated_gb <= vram_gb:
|
|
63
|
+
fits.append(quant)
|
|
64
|
+
|
|
65
|
+
return fits
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
<#
|
|
2
|
+
.SYNOPSIS
|
|
3
|
+
Downloads a GGUF model file from HuggingFace using direct IP connection.
|
|
4
|
+
|
|
5
|
+
.DESCRIPTION
|
|
6
|
+
Alternative download method that connects to HuggingFace CDN servers
|
|
7
|
+
directly via IP address using PowerShell 7's .NET SslStream.
|
|
8
|
+
Useful when standard HTTPS downloads fail due to SSL or routing issues.
|
|
9
|
+
|
|
10
|
+
Requires PowerShell 7 (pwsh) and an elevated terminal.
|
|
11
|
+
|
|
12
|
+
.PARAMETER Repo
|
|
13
|
+
HuggingFace repo (e.g., "bartowski/Qwen2.5-Coder-7B-Instruct-GGUF").
|
|
14
|
+
|
|
15
|
+
.PARAMETER File
|
|
16
|
+
Exact GGUF filename. If omitted, lists available files.
|
|
17
|
+
|
|
18
|
+
.PARAMETER OutDir
|
|
19
|
+
Output directory. Defaults to script's directory.
|
|
20
|
+
|
|
21
|
+
.EXAMPLE
|
|
22
|
+
pwsh -ExecutionPolicy Bypass -File HfDownload.ps1 "bartowski/Qwen2.5-Coder-7B-Instruct-GGUF" "Qwen2.5-Coder-7B-Instruct-Q4_K_M.gguf"
|
|
23
|
+
#>
|
|
24
|
+
param(
|
|
25
|
+
[Parameter(Mandatory, Position=0)]
|
|
26
|
+
[string]$Repo,
|
|
27
|
+
|
|
28
|
+
[Parameter(Position=1)]
|
|
29
|
+
[string]$File,
|
|
30
|
+
|
|
31
|
+
[string]$OutDir
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
$ErrorActionPreference = 'Stop'
|
|
35
|
+
if (-not $OutDir) { $OutDir = $PSScriptRoot }
|
|
36
|
+
|
|
37
|
+
$US_EAST_IPS = @(
|
|
38
|
+
"35.173.17.142", "34.231.87.187", "44.217.206.136",
|
|
39
|
+
"3.216.102.62", "34.230.200.86", "100.50.185.240",
|
|
40
|
+
"100.57.83.12", "98.90.123.60", "100.31.16.245", "100.29.213.216"
|
|
41
|
+
)
|
|
42
|
+
$RouteExe = "$env:SystemRoot\System32\route.exe"
|
|
43
|
+
|
|
44
|
+
# --- Find Wi-Fi gateway ---
|
|
45
|
+
|
|
46
|
+
function Find-WifiGateway {
|
|
47
|
+
$output = & $RouteExe print -4 0.0.0.0 2>&1 | Out-String
|
|
48
|
+
$routes = @()
|
|
49
|
+
foreach ($line in $output -split "`n") {
|
|
50
|
+
$parts = $line.Trim() -split '\s+'
|
|
51
|
+
if ($parts.Count -ge 5 -and $parts[0] -eq '0.0.0.0' -and $parts[1] -eq '0.0.0.0' -and $parts[2] -ne 'On-link') {
|
|
52
|
+
$routes += @{ Gateway = $parts[2]; Metric = [int]$parts[4] }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if ($routes.Count -eq 0) { return $null }
|
|
56
|
+
$routes | Sort-Object { $_.Metric } -Descending | Select-Object -First 1 -ExpandProperty Gateway
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function Find-WifiIfIndex {
|
|
60
|
+
$ipconfig = & "$env:SystemRoot\System32\ipconfig.exe" 2>&1 | Out-String
|
|
61
|
+
$inWifi = $false
|
|
62
|
+
foreach ($line in $ipconfig -split "`n") {
|
|
63
|
+
if ($line -match 'Wi-Fi|Wireless') { $inWifi = $true }
|
|
64
|
+
elseif ($inWifi -and $line -match '%(\d+)') { return [int]$Matches[1] }
|
|
65
|
+
elseif ($inWifi -and $line.Trim() -and -not $line.StartsWith(' ')) { $inWifi = $false }
|
|
66
|
+
}
|
|
67
|
+
return 0
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# --- Route management ---
|
|
71
|
+
|
|
72
|
+
$script:routes = @()
|
|
73
|
+
|
|
74
|
+
function Add-Route([string]$IP, [string]$Gateway, [int]$IfIdx) {
|
|
75
|
+
$args2 = "add $IP mask 255.255.255.255 $Gateway"
|
|
76
|
+
if ($IfIdx -gt 0) { $args2 += " IF $IfIdx" }
|
|
77
|
+
$args2 += " metric 1"
|
|
78
|
+
& $RouteExe $args2.Split(' ') 2>&1 | Out-Null
|
|
79
|
+
$script:routes += $IP
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function Remove-AllRoutes {
|
|
83
|
+
foreach ($ip in $script:routes) { & $RouteExe delete $ip 2>&1 | Out-Null }
|
|
84
|
+
$script:routes = @()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
# --- Main ---
|
|
88
|
+
|
|
89
|
+
Write-Host "`n[1/3] Setup..." -ForegroundColor Cyan
|
|
90
|
+
$gateway = Find-WifiGateway
|
|
91
|
+
if (-not $gateway) { Write-Error "Cannot find Wi-Fi gateway. Ensure Wi-Fi is connected." }
|
|
92
|
+
$ifIdx = Find-WifiIfIndex
|
|
93
|
+
Write-Host " Gateway: $gateway (IF $ifIdx)"
|
|
94
|
+
|
|
95
|
+
# List files if not specified
|
|
96
|
+
if (-not $File) {
|
|
97
|
+
Write-Host "`n Fetching files from huggingface.co/$Repo..." -ForegroundColor DarkGray
|
|
98
|
+
$response = Invoke-RestMethod -Uri "https://huggingface.co/api/models/$Repo/tree/main" -TimeoutSec 30
|
|
99
|
+
$ggufFiles = @($response | Where-Object { $_.path -like '*.gguf' -and $_.type -eq 'file' } |
|
|
100
|
+
Select-Object @{N='Name';E={$_.path}}, @{N='SizeGB';E={[math]::Round($_.size/1GB,2)}} | Sort-Object SizeGB)
|
|
101
|
+
if ($ggufFiles.Count -eq 0) { Write-Error "No GGUF files in $Repo" }
|
|
102
|
+
Write-Host "`n Available:" -ForegroundColor Yellow
|
|
103
|
+
for ($i=0; $i -lt $ggufFiles.Count; $i++) {
|
|
104
|
+
$f = $ggufFiles[$i]
|
|
105
|
+
$lbl = if ($f.Name -match 'Q4_K_M') {' (recommended)'} else {''}
|
|
106
|
+
Write-Host " [$($i+1)] $($f.Name) ($($f.SizeGB) GB)$lbl"
|
|
107
|
+
}
|
|
108
|
+
$choice = Read-Host "`n Select [1-$($ggufFiles.Count)]"
|
|
109
|
+
$File = $ggufFiles[[int]$choice - 1].Name
|
|
110
|
+
}
|
|
111
|
+
Write-Host " File: $File" -ForegroundColor Green
|
|
112
|
+
|
|
113
|
+
# Get redirect URL
|
|
114
|
+
Write-Host "`n[2/3] Getting download URL..." -ForegroundColor Cyan
|
|
115
|
+
$hfUrl = "https://huggingface.co/$Repo/resolve/main/$File"
|
|
116
|
+
$redirectUrl = $null
|
|
117
|
+
try { Invoke-WebRequest -Uri $hfUrl -Method HEAD -MaximumRedirection 0 -ErrorAction Stop } catch {
|
|
118
|
+
$redirectUrl = $_.Exception.Response.Headers.Location.ToString()
|
|
119
|
+
}
|
|
120
|
+
if (-not $redirectUrl) { Write-Error "Failed to get redirect URL from HuggingFace." }
|
|
121
|
+
$cdnUri = [System.Uri]$redirectUrl
|
|
122
|
+
Write-Host " CDN: $($cdnUri.Host)"
|
|
123
|
+
|
|
124
|
+
# Build candidate IPs
|
|
125
|
+
$candidates = @()
|
|
126
|
+
if ($cdnUri.PathAndQuery -match 'xet-bridge-us') {
|
|
127
|
+
$candidates += $US_EAST_IPS
|
|
128
|
+
Write-Host " US bridge detected; using US East IPs" -ForegroundColor DarkGray
|
|
129
|
+
}
|
|
130
|
+
$dnsIPs = @((Resolve-DnsName $cdnUri.Host -Type A -ErrorAction SilentlyContinue).IPAddress)
|
|
131
|
+
foreach ($ip in $dnsIPs) { if ($ip -notin $candidates) { $candidates += $ip } }
|
|
132
|
+
|
|
133
|
+
# Add routes
|
|
134
|
+
foreach ($ip in $candidates) { Add-Route $ip $gateway $ifIdx }
|
|
135
|
+
|
|
136
|
+
# Download
|
|
137
|
+
Write-Host "`n[3/3] Downloading..." -ForegroundColor Cyan
|
|
138
|
+
$destPath = Join-Path $OutDir $File
|
|
139
|
+
$success = $false
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
foreach ($ip in $candidates) {
|
|
143
|
+
try {
|
|
144
|
+
Write-Host " Trying $ip... " -NoNewline
|
|
145
|
+
|
|
146
|
+
$tcp = New-Object System.Net.Sockets.TcpClient
|
|
147
|
+
$tcp.ReceiveBufferSize = 8 * 1024 * 1024
|
|
148
|
+
$tcp.Connect($ip, 443)
|
|
149
|
+
|
|
150
|
+
$ssl = New-Object System.Net.Security.SslStream($tcp.GetStream(), $false, ({$true}))
|
|
151
|
+
$ssl.AuthenticateAsClient($ip)
|
|
152
|
+
|
|
153
|
+
# Verify the cert is from the expected CDN, not a proxy
|
|
154
|
+
$issuer = $ssl.RemoteCertificate.Issuer
|
|
155
|
+
if ($issuer -notmatch 'Amazon|DigiCert|Let''s Encrypt|Google Trust|Cloudflare') {
|
|
156
|
+
Write-Host "unexpected cert issuer" -ForegroundColor Yellow
|
|
157
|
+
$ssl.Close(); $tcp.Close(); continue
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
# Send GET
|
|
161
|
+
$req = "GET $($cdnUri.PathAndQuery) HTTP/1.1`r`nHost: $($cdnUri.Host)`r`nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)`r`nAccept: */*`r`nConnection: keep-alive`r`n`r`n"
|
|
162
|
+
$ssl.Write([System.Text.Encoding]::ASCII.GetBytes($req)); $ssl.Flush()
|
|
163
|
+
|
|
164
|
+
# Read headers
|
|
165
|
+
$headerBytes = New-Object System.Collections.Generic.List[byte]
|
|
166
|
+
$prev = [byte[]]::new(4)
|
|
167
|
+
while ($true) {
|
|
168
|
+
$b = $ssl.ReadByte(); if ($b -eq -1) { throw "Connection closed" }
|
|
169
|
+
$headerBytes.Add([byte]$b)
|
|
170
|
+
$prev[0]=$prev[1];$prev[1]=$prev[2];$prev[2]=$prev[3];$prev[3]=[byte]$b
|
|
171
|
+
if ($prev[0]-eq13 -and $prev[1]-eq10 -and $prev[2]-eq13 -and $prev[3]-eq10) { break }
|
|
172
|
+
}
|
|
173
|
+
$hdr = [System.Text.Encoding]::ASCII.GetString($headerBytes.ToArray())
|
|
174
|
+
$status = ($hdr -split "`r`n")[0]
|
|
175
|
+
|
|
176
|
+
if ($status -notmatch '200') {
|
|
177
|
+
Write-Host $status -ForegroundColor Yellow
|
|
178
|
+
$ssl.Close(); $tcp.Close(); continue
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
$contentLength = [long]0
|
|
182
|
+
if ($hdr -match 'content-length:\s*(\d+)') { $contentLength = [long]$Matches[1] }
|
|
183
|
+
Write-Host "200 OK ($([math]::Round($contentLength/1GB,2)) GB)" -ForegroundColor Green
|
|
184
|
+
Write-Host ""
|
|
185
|
+
|
|
186
|
+
# Stream to file
|
|
187
|
+
$fs = [System.IO.File]::Create($destPath)
|
|
188
|
+
$buf = New-Object byte[] (4*1024*1024)
|
|
189
|
+
$total = [long]0; $sw = [System.Diagnostics.Stopwatch]::StartNew(); $lr = [long]0
|
|
190
|
+
|
|
191
|
+
try {
|
|
192
|
+
while ($total -lt $contentLength) {
|
|
193
|
+
$n = [int][math]::Min([long]$buf.Length, [long]($contentLength - $total))
|
|
194
|
+
$read = $ssl.Read($buf, 0, $n); if ($read -eq 0) { break }
|
|
195
|
+
$fs.Write($buf, 0, $read); $total += $read
|
|
196
|
+
if ($total - $lr -gt 100MB) {
|
|
197
|
+
$pct = [math]::Round($total*100/$contentLength,1)
|
|
198
|
+
$spd = if($sw.Elapsed.TotalSeconds -gt 0){[math]::Round($total/$sw.Elapsed.TotalSeconds/1MB,1)}else{0}
|
|
199
|
+
$eta = if($spd -gt 0){$r=($contentLength-$total)/($spd*1MB);if($r -gt 60){"$([math]::Round($r/60,1))m"}else{"$([math]::Round($r))s"}}else{'?'}
|
|
200
|
+
Write-Host " [$pct%] $([math]::Round($total/1GB,2))GB @ ${spd}MB/s ETA:$eta"
|
|
201
|
+
$lr = $total
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
Write-Host " [100%] Done! $([math]::Round($total/1GB,2)) GB in $([math]::Round($sw.Elapsed.TotalMinutes,1)) min" -ForegroundColor Green
|
|
205
|
+
} finally { $fs.Close(); $ssl.Close(); $tcp.Close() }
|
|
206
|
+
|
|
207
|
+
if ($total -ge $contentLength * 0.99) { $success = $true }
|
|
208
|
+
else { Write-Warning "Incomplete: $total / $contentLength bytes" }
|
|
209
|
+
break
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
Write-Host "FAILED ($($_.Exception.Message))" -ForegroundColor Red
|
|
213
|
+
continue
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
} finally {
|
|
217
|
+
Remove-AllRoutes
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (-not $success) { Write-Error "All $($candidates.Count) CDN IPs failed." }
|
|
221
|
+
Write-Host "`n Saved: $destPath" -ForegroundColor Green
|