param( [string]$InstallRoot = (Join-Path $env:LOCALAPPDATA "MINIKE-Local-AI"), [ValidateSet("editors", "creator", "full")][string]$Preset = "editors", [string[]]$Modules = @(), [string]$BaseUrl = "https://minike.com.br/local-ai", [switch]$SkipModelDownloads, [switch]$ValidateOnly ) $ErrorActionPreference = "Stop" $machinePath=[Environment]::GetEnvironmentVariable("Path","Machine");$userPath=[Environment]::GetEnvironmentVariable("Path","User");$env:Path=@($env:Path,$machinePath,$userPath)-join ";" $catalog = [ordered]@{ ffmpeg=@{kind="winget";id="Gyan.FFmpeg";command="ffmpeg"}; imagemagick=@{kind="winget";id="ImageMagick.ImageMagick";command="magick"} libreoffice=@{kind="winget";id="TheDocumentFoundation.LibreOffice";command="soffice"}; tesseract=@{kind="winget";id="UB-Mannheim.TesseractOCR";command="tesseract"} comfyui=@{kind="mike_script";script="install-comfyui-mike.ps1"}; invokeai=@{kind="guide";url="https://invoke-ai.github.io/InvokeAI/installation/quick_start/"} forge=@{kind="git";repo="https://github.com/lllyasviel/stable-diffusion-webui-forge.git"}; fooocus=@{kind="git";repo="https://github.com/lllyasviel/Fooocus.git"}; whisper_cpp=@{kind="whisper_release";repo="https://github.com/ggml-org/whisper.cpp"} maps_leads=@{kind="git";repo="https://github.com/gosom/google-maps-scraper.git"}; ltx_video=@{kind="git";repo="https://github.com/Lightricks/LTX-Video.git"} wan_video=@{kind="git";repo="https://github.com/Wan-Video/Wan2.1.git"}; cogvideo=@{kind="git";repo="https://github.com/zai-org/CogVideo.git"} stirling_pdf=@{kind="guide";url="https://github.com/Stirling-Tools/Stirling-PDF"}; remotion=@{kind="guide";url="https://www.remotion.dev/docs/"} } $presets=@{editors=@("ffmpeg","imagemagick","libreoffice","tesseract","whisper_cpp");creator=@("ffmpeg","imagemagick","comfyui","forge","fooocus","whisper_cpp","remotion");full=@($catalog.Keys)} function Has-Command([string]$Name){return [bool](Get-Command $Name -ErrorAction SilentlyContinue)} function Refresh-ProcessPath {$machine=[Environment]::GetEnvironmentVariable("Path","Machine");$user=[Environment]::GetEnvironmentVariable("Path","User");$env:Path=@($env:Path,$machine,$user,"$env:LOCALAPPDATA\Programs\Python\Python311","$env:LOCALAPPDATA\Programs\Python\Python311\Scripts")-join ";"} function Get-RealPython { $candidates=@();$launcher=Get-Command py.exe -ErrorAction SilentlyContinue;if($launcher){$candidates+=@(@{path=$launcher.Source;prefix=@("-3.11")},@{path=$launcher.Source;prefix=@("-3")})};$python=Get-Command python.exe -ErrorAction SilentlyContinue;if($python){$candidates+=@(@{path=$python.Source;prefix=@()})};$candidates+=Get-ChildItem "$env:LOCALAPPDATA\Programs\Python\Python3*\python.exe" -ErrorAction SilentlyContinue|ForEach-Object{@{path=$_.FullName;prefix=@()}} foreach($candidate in $candidates){try{$version=& $candidate.path @($candidate.prefix) --version 2>&1;if($LASTEXITCODE-eq0-and($version-join" ")-match"Python 3\.(10|11|12)(\.|$)"){return [string]$candidate.path}}catch{}};return "" } function Ensure-InstallerPrerequisites { $winget=Get-Command winget.exe -ErrorAction SilentlyContinue if(-not(Has-Command "git")){if(-not$winget){throw "Git ausente e winget indisponivel."};& $winget.Source install --id Git.Git -e --silent --accept-package-agreements --accept-source-agreements --disable-interactivity|Out-Host;if($LASTEXITCODE-ne0){throw "Falha ao instalar Git."};Refresh-ProcessPath} if(-not(Get-RealPython)){if(-not$winget){throw "Python 3.11 ausente e winget indisponivel."};& $winget.Source install --id Python.Python.3.11 -e --silent --accept-package-agreements --accept-source-agreements --disable-interactivity|Out-Host;if($LASTEXITCODE-ne0){throw "Falha ao instalar Python 3.11."};Refresh-ProcessPath} if(-not(Has-Command "git")){throw "Git foi instalado, mas nao ficou acessivel neste processo."};$python=Get-RealPython;if(-not$python){throw "Python 3.11 foi instalado, mas o executavel real nao foi localizado."};return $python } function Get-ComfyProfile { $gpu=@(Get-CimInstance Win32_VideoController -ErrorAction SilentlyContinue|ForEach-Object{[string]$_.Name})-join" | ";$ram=0;try{$ram=[math]::Round((Get-CimInstance Win32_ComputerSystem).TotalPhysicalMemory/1GB,1)}catch{} if($gpu-match"NVIDIA|RTX|GTX"-and$ram-ge16){return "gpu"};if($gpu-match"NVIDIA|RTX|GTX|AMD|Radeon|Arc"){return "low_vram"};return "cpu" } function Install-Winget($Module,$Spec){ if(Has-Command $Spec.command){return @{module=$Module;status="already_present"}} if(-not(Has-Command "winget")){return @{module=$Module;status="guided";reason="winget_missing"}} & winget install --id $Spec.id -e --accept-package-agreements --accept-source-agreements --disable-interactivity | Out-Host if($LASTEXITCODE-ne0){ try{ $listed=& winget list --id $Spec.id -e 2>$null if($LASTEXITCODE-eq0 -and ($listed -join "`n") -match [regex]::Escape($Spec.id)){Refresh-ProcessPath;return @{module=$Module;status="package_present_path_pending";command=$Spec.command}} }catch{} throw "Falha ao instalar $Module pelo winget (codigo $LASTEXITCODE)." };Refresh-ProcessPath return @{module=$Module;status=$(if(Has-Command $Spec.command){"installed"}else{"restart_or_path_pending"})} } function Install-GitSource($Module,$Spec){ if(-not(Has-Command "git")){return @{module=$Module;status="guided";reason="git_missing";source=$Spec.repo}} $target=Join-Path (Join-Path $InstallRoot "secondary-engines") $Module if(-not(Test-Path -LiteralPath (Join-Path $target ".git"))){& git clone --depth 1 $Spec.repo $target;if($LASTEXITCODE-ne0){throw "Falha ao baixar $Module."}} return @{module=$Module;status="source_ready";root=$target;models_required=($Module -in @("ltx_video","wan_video","cogvideo","forge","fooocus"))} } function Install-ComfyCustomNodes { $comfy=Join-Path $InstallRoot "ComfyUI";$nodes=Join-Path $comfy "custom_nodes" if(-not(Test-Path (Join-Path $comfy "main.py"))){return @{module="comfy_custom_nodes";status="skipped";reason="comfyui_missing"}} $repos=@( @{name="ComfyUI-Manager";url="https://github.com/Comfy-Org/ComfyUI-Manager.git"}, @{name="ComfyUI-WanVideoWrapper";url="https://github.com/kijai/ComfyUI-WanVideoWrapper.git"}, @{name="ComfyUI-LTXVideo";url="https://github.com/Lightricks/ComfyUI-LTXVideo.git"}, @{name="ComfyUI-Frame-Interpolation";url="https://github.com/Fannovel16/ComfyUI-Frame-Interpolation.git"} ) New-Item -ItemType Directory -Force -Path $nodes|Out-Null foreach($repo in $repos){$target=Join-Path $nodes $repo.name;if(-not(Test-Path (Join-Path $target ".git"))){& git clone --depth 1 $repo.url $target;if($LASTEXITCODE-ne0){throw "Falha ao clonar $($repo.name)"}}} $py=Join-Path $comfy ".venv\Scripts\python.exe" if(Test-Path $py){foreach($req in Get-ChildItem $nodes -Recurse -Filter requirements.txt){& $py -m pip install -r $req.FullName;if($LASTEXITCODE-ne0){throw "Falha pip requirements: $($req.FullName)"}}} return @{module="comfy_custom_nodes";status="ready";nodes=@($repos.name)} } function Install-WhisperCpp { $target=Join-Path (Join-Path $InstallRoot "secondary-engines") "whisper_cpp" $bin=Join-Path $target "bin\whisper-cli.exe";$model=Join-Path $target "models\ggml-tiny.bin" New-Item -ItemType Directory -Force -Path (Split-Path $bin -Parent),(Split-Path $model -Parent)|Out-Null if(-not(Test-Path -LiteralPath $bin)){ $headers=@{"User-Agent"="Mike-Local-Installer"};$release=Invoke-RestMethod -Headers $headers -Uri "https://api.github.com/repos/ggml-org/whisper.cpp/releases/latest" -TimeoutSec 45 $asset=@($release.assets|Where-Object{$_.name-eq"whisper-bin-x64.zip"}|Select-Object -First 1) if(-not $asset){throw "Release oficial whisper.cpp sem pacote Windows x64."} $zip=Join-Path $env:TEMP "mike-whisper-x64.zip";$extract=Join-Path $env:TEMP "mike-whisper-x64" Invoke-WebRequest -UseBasicParsing -Uri $asset.browser_download_url -OutFile $zip -TimeoutSec 180 if(Test-Path $extract){Remove-Item $extract -Recurse -Force};Expand-Archive -LiteralPath $zip -DestinationPath $extract -Force $exe=Get-ChildItem $extract -Recurse -Filter "whisper-cli.exe"|Select-Object -First 1;if(-not $exe){throw "whisper-cli.exe ausente no pacote oficial."} Copy-Item (Join-Path $exe.Directory.FullName "*") (Split-Path $bin -Parent) -Recurse -Force;Remove-Item $zip,$extract -Recurse -Force } if(-not(Test-Path -LiteralPath $model)){ Invoke-WebRequest -UseBasicParsing -Uri "https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.bin" -OutFile $model -TimeoutSec 300 } if((Get-Item $model).Length-lt 70MB){throw "Modelo Whisper tiny incompleto."} return @{module="whisper_cpp";status="installed";runtime=$bin;model=$model;model_size=(Get-Item $model).Length} } $selected=if($Modules.Count){@($Modules)}else{@($presets[$Preset])} $unknown=@($selected|Where-Object{-not $catalog.Contains($_)});if($unknown.Count){throw "Modulos desconhecidos: $($unknown -join ', ')"} if($ValidateOnly){[pscustomobject]@{ok=$true;validate_only=$true;preset=$Preset;modules=$selected;install_root=[IO.Path]::GetFullPath($InstallRoot)};return} $InstallRoot=[IO.Path]::GetFullPath($InstallRoot);New-Item -ItemType Directory -Force -Path $InstallRoot,(Join-Path $InstallRoot "secondary-engines")|Out-Null $pythonPath=Ensure-InstallerPrerequisites;$comfyProfile=Get-ComfyProfile $agentName="mike-secondary-engines-agent.ps1";$agentSource=Join-Path $PSScriptRoot $agentName;$agentTarget=Join-Path $InstallRoot $agentName if(Test-Path -LiteralPath $agentSource){if([IO.Path]::GetFullPath($agentSource)-ne[IO.Path]::GetFullPath($agentTarget)){Copy-Item -LiteralPath $agentSource -Destination $agentTarget -Force}}else{(New-Object Net.WebClient).DownloadFile($BaseUrl.TrimEnd("/")+"/"+$agentName,$agentTarget)} $heavyName="install-mike-heavy-models.ps1";$heavySource=Join-Path $PSScriptRoot $heavyName;$heavyTarget=Join-Path $InstallRoot $heavyName if(Test-Path -LiteralPath $heavySource){if([IO.Path]::GetFullPath($heavySource)-ne[IO.Path]::GetFullPath($heavyTarget)){Copy-Item -LiteralPath $heavySource -Destination $heavyTarget -Force}}else{(New-Object Net.WebClient).DownloadFile($BaseUrl.TrimEnd("/")+"/"+$heavyName,$heavyTarget)} $modelManifestName="mike-secondary-models.json";$modelManifestSource=Join-Path $PSScriptRoot $modelManifestName;$modelManifestTarget=Join-Path $InstallRoot $modelManifestName if(Test-Path -LiteralPath $modelManifestSource){if([IO.Path]::GetFullPath($modelManifestSource)-ne[IO.Path]::GetFullPath($modelManifestTarget)){Copy-Item -LiteralPath $modelManifestSource -Destination $modelManifestTarget -Force}}else{(New-Object Net.WebClient).DownloadFile($BaseUrl.TrimEnd("/")+"/"+$modelManifestName,$modelManifestTarget)} $results=@();foreach($module in $selected){ $spec=$catalog[$module];Write-Host ("[Mike secundarios] Preparando "+$module) try { if($spec.kind-eq"winget"){$results+=Install-Winget $module $spec;continue} if($spec.kind-eq"git"){$results+=Install-GitSource $module $spec;continue} if($spec.kind-eq"mike_script"){$scriptPath=Join-Path $InstallRoot $spec.script;if(-not(Test-Path $scriptPath)){(New-Object Net.WebClient).DownloadFile($BaseUrl.TrimEnd("/")+"/"+$spec.script,$scriptPath)};& powershell.exe -NoProfile -ExecutionPolicy Bypass -File $scriptPath -InstallRoot (Join-Path $InstallRoot "ComfyUI") -Profile $comfyProfile -PythonPath $pythonPath;if($LASTEXITCODE-ne0){throw "Falha ao instalar ComfyUI."};$results+=@{module=$module;status="runtime_ready_model_required";profile=$comfyProfile};continue} if($spec.kind-eq"whisper_release"){$results+=Install-WhisperCpp;continue} if($spec.kind-eq"cmake"){$result=Install-GitSource $module $spec;if((Has-Command "cmake")-and$result.root){& cmake -S $result.root -B (Join-Path $result.root "build") -DBUILD_SHARED_LIBS=OFF;& cmake --build (Join-Path $result.root "build") --config Release;$result.status="runtime_ready_model_required"}else{$result.status="source_ready_cmake_required"};$results+=$result;continue} $results+=@{module=$module;status="guided";docs=$spec.url} } catch { # Tolerar falha por modulo para que o preset=full nao aborte o instalador inteiro. # O report agrega o status, e o monstro pai ja trata exit code != 0 apenas como aviso para presets nao-criticos. $results+=@{module=$module;status="failed";error=$_.Exception.Message} Write-Host ("[Mike secundarios] Falha em "+$module+" : "+$_.Exception.Message) -ForegroundColor Yellow } } if($Preset -in @("creator","full")){ try { $results+=Install-ComfyCustomNodes } catch { $results+=@{module="comfy_custom_nodes";status="failed";error=$_.Exception.Message}; Write-Host "[Mike secundarios] comfy_custom_nodes falhou: $($_.Exception.Message)" -ForegroundColor Yellow } } if(-not $SkipModelDownloads -and $Preset -in @("creator","full")){ $profile=if($Preset-eq"full"){"full"}else{"creator"} try { $results+=@{module="heavy_models";status="running";profile=$profile} & $heavyTarget -InstallRoot $InstallRoot -Profile $profile -ManifestPath $modelManifestTarget if($LASTEXITCODE-ne0){throw "Falha ao baixar modelos pesados (codigo $LASTEXITCODE)."} } catch { $results+=@{module="heavy_models";status="failed";profile=$profile;error=$_.Exception.Message} Write-Host "[Mike secundarios] heavy_models falhou: $($_.Exception.Message)" -ForegroundColor Yellow } } $report=[ordered]@{ok=$true;installed_at=(Get-Date).ToString("o");preset=$Preset;modules=$results;note="Preset editors evita peso. Presets creator/full baixam runtimes e modelos necessarios, salvo quando -SkipModelDownloads for usado."} $report|ConvertTo-Json -Depth 8|Set-Content -LiteralPath (Join-Path $InstallRoot "secondary-engines-report.json") -Encoding UTF8;$report