函数/New-MAI图像.ps1
|
function New-MAI图像 { <# .SYNOPSIS 调用微软 MAI 图像模型生成图像。 .DESCRIPTION 走 OpenRouter 统一图像接口(POST /api/v1/images)调用 MAI-Image-2.5 (microsoft/mai-image-2.5,由 Azure AI Foundry 提供服务)。 图像以 base64 返回(data[0].b64_json),media_type 标识实际格式(通常为 PNG)。 密钥、基础地址与模型以 DPAPI 加密记住。计费为全包制:成功出图整张计费,失败不计费。 mai-image-2.5 端点能力: - 参考图(input_references):最多 1 张。 - 宽高比:仅 1:1、4:3、3:4、16:9、9:16、3:2、2:3、auto。 - 单次出图张数(n):固定 1。 - 上游 Azure 限制:宽高各 ≥768,总像素 ≤1,048,576(约 1024×1024),输出固定 PNG。 .PARAMETER 提示词 图像描述。与 -提示文件 二选一。 .PARAMETER 提示文件 提示词文本文件路径。与 -提示词 二选一。 .PARAMETER 密钥 交互式输入密钥开关。指定此开关时会提示输入(OpenRouter 密钥形如 sk-or-...)。 .PARAMETER 密钥值 API 令牌明文。未指定时查找已记住的值。 .PARAMETER 基础地址 OpenRouter Base URL。默认 https://openrouter.ai。 .PARAMETER 模型 OpenRouter 模型 ID。默认 microsoft/mai-image-2.5。 .PARAMETER 参考图 参考图像路径或 URL,用于图生图编辑(input_references)。 mai-image-2.5 端点最多支持 1 张; 本地文件自动转为 base64 data URI,URL 直接透传。 .PARAMETER 宽高比 mai-image-2.5 支持:1:1 | 4:3 | 3:4 | 16:9 | 9:16 | 3:2 | 2:3 | auto。默认不发送。 .PARAMETER 尺寸 分辨率档位(512 | 1K | 2K | 4K)或精确像素 WxH。默认不发送。 .PARAMETER 数量 一次生成几张(1~10,并非所有上游支持 >1)。默认 1。 .PARAMETER 输出路径 输出文件路径。默认时间戳 PNG。多张时自动追加序号。 .PARAMETER 超时秒数 超时时间,默认 500。 .EXAMPLE New-MAI图像 -提示词 "水彩柴犬" -密钥值 'sk-or-xxx' .EXAMPLE New-MAI图像 -提示词 "把这张照片改成吉卜力动画风格" -参考图 .\照片.png .EXAMPLE New-MAI图像 -提示词 "电影海报" -宽高比 16:9 -数量 2 #> [CmdletBinding(DefaultParameterSetName = '提示词')] param( [Parameter(Mandatory = $true, Position = 0, ParameterSetName = '提示词')] [ValidateNotNullOrEmpty()] [string]$提示词, [Parameter(Mandatory = $true, ParameterSetName = '提示文件')] [ValidateNotNullOrEmpty()] [string]$提示文件, [Parameter()] [switch]$密钥, [Parameter()][string]$密钥值, [Parameter()][string]$基础地址, [Parameter()][string]$模型, [Parameter()] [ValidateNotNullOrEmpty()] [string[]]$参考图, [Parameter()][string]$宽高比 = '', [Parameter()][string]$尺寸 = '', [Parameter()][ValidateRange(1, 10)] [int]$数量 = 1, [Parameter()] [string]$输出路径 = ".\mai_$(Get-Date -Format 'yyyyMMdd_HHmmss').png", [Parameter()][int]$超时秒数 = 500 ) $配置路径 = Join-Path $env:LOCALAPPDATA 'Image-generation-cli\AI图像生成-MAI配置.xml' # 提示词 if ($PSCmdlet.ParameterSetName -eq '提示文件') { $提示词 = Read-提示文件 -路径 $提示文件 } # 凭据 if ($密钥.IsPresent -and -not $密钥值) { $安全密钥 = Read-Host -Prompt '请输入 OpenRouter API 密钥(sk-or-...)' -AsSecureString $密钥值 = [System.Net.NetworkCredential]::new([string]::Empty, $安全密钥).Password } $记住的配置 = Import-记住的配置 -配置路径 $配置路径 # 默认值:基础地址与模型 if (-not $基础地址 -and -not ($记住的配置 -and $记住的配置.基础地址)) { $基础地址 = 'https://openrouter.ai' } if (-not $MyInvocation.BoundParameters.ContainsKey('模型') -and -not ($记住的配置 -and $记住的配置.模型)) { $模型 = 'microsoft/mai-image-2.5' } $凭据 = Resolve-配置凭据 -参数密钥 $密钥值 -参数基础地址 $基础地址 -参数模型 $模型 ` -记住的配置 $记住的配置 -配置路径 $配置路径 # 端点:基础地址已带 /api/vN 后缀则直接拼 images,否则补 /api/v1 if ($凭据.基础地址 -match '/api/v\d+/?$') { $api基础 = $凭据.基础地址.TrimEnd('/') } else { $api基础 = $凭据.基础地址.TrimEnd('/') + '/api/v1' } $端点 = "$api基础/images" # 参考图:URL 直接透传,本地文件编码为 base64 data URI $参考引用列表 = [System.Collections.Generic.List[object]]::new() if ($参考图) { foreach ($单张 in $参考图) { if ($单张 -match '^https?://') { $引用URL = $单张 } else { $数据 = Get-图像数据 -来源 $单张 $mime = Get-Mime类型 -路径 $数据.文件名 if ($mime -eq 'application/octet-stream') { throw "参考图格式无法识别:$单张" } $引用URL = "data:$mime;base64," + [Convert]::ToBase64String($数据.字节) } $参考引用列表.Add(@{ type = 'image_url'; image_url = @{ url = $引用URL } }) } } # 请求体(OpenRouter Image API) $请求体对象 = [ordered]@{ model = $凭据.模型 prompt = $提示词 } if ($参考引用列表.Count -gt 0) { $参考引用数组 = $参考引用列表.ToArray() $请求体对象['input_references'] = $参考引用数组 } if ($宽高比) { $请求体对象['aspect_ratio'] = $宽高比 } if ($尺寸) { $请求体对象['size'] = $尺寸 } if ($数量 -gt 1) { $请求体对象['n'] = $数量 } $请求体 = $请求体对象 | ConvertTo-Json -Depth 10 $请求头 = @{ 'Authorization' = "Bearer $($凭据.密钥明文)"; 'Content-Type' = 'application/json' } Write-Host "正在调用 $($凭据.模型) 生成图像 ..." -ForegroundColor Cyan try { $响应 = Invoke-RestMethod -Uri $端点 -Method Post -Headers $请求头 ` -Body ([System.Text.Encoding]::UTF8.GetBytes($请求体)) -TimeoutSec $超时秒数 } catch { # ErrorDetails 非空时即为服务端响应体;否则展开异常链提取。 # 不翻译、不包装,原样抛出。 $错误详对象 = $_.ErrorDetails if ($错误详对象 -and $错误详对象.PSObject.Properties['Message'] -and $错误详对象.Message) { $错误详情 = $错误详对象.Message } else { $错误详情 = Get-NetException详情 -Exception $_.Exception } throw $错误详情 } # 记住凭据 if ($凭据.密钥来源 -ne '已记住' -or $凭据.基础地址来源 -ne '已记住' -or $凭据.模型来源 -ne '已记住') { try { Save-配置 -安全密钥 $凭据.安全密钥对象 -保存基础地址 $凭据.基础地址 -保存模型 $凭据.模型 -配置路径 $配置路径 Write-Host "密钥、基础地址与模型已记住:$配置路径" -ForegroundColor DarkGray } catch { Write-Warning "配置保存失败:$($_.Exception.Message)" } } # 解析响应:data[] 中的 b64_json $数据属性 = $响应.PSObject.Properties['data'] if (-not $数据属性 -or -not $数据属性.Value -or @($数据属性.Value).Count -eq 0) { throw "API 未返回图像数据:$($响应 | ConvertTo-Json -Depth 10 -Compress)" } $图像列表 = @($数据属性.Value) # 费用信息 $usage属性 = $响应.PSObject.Properties['usage'] if ($usage属性 -and $usage属性.Value) { $费用属性 = $usage属性.Value.PSObject.Properties['cost'] if ($费用属性 -and $费用属性.Value -ne $null) { # PS 5.1 的 ConvertFrom-Json 数字可能为对象,先转字符串再转 double Write-Host ("本次费用: $" + [double]"$($费用属性.Value)") -ForegroundColor DarkGray } } # 保存输出:media_type 决定扩展名;多张时追加序号 $解析后输出 = Resolve-输出路径 -输出路径 $输出路径 $序号 = 0 $保存路径列表 = @() foreach ($图像 in $图像列表) { $序号++ $当前路径 = $解析后输出 if ($图像列表.Count -gt 1) { $无扩展名 = [System.IO.Path]::GetFileNameWithoutExtension($解析后输出) $扩展名 = [System.IO.Path]::GetExtension($解析后输出) $当前路径 = Join-Path (Split-Path -Parent $解析后输出) "${无扩展名}_${序号}${扩展名}" $目录 = Split-Path -Parent $当前路径 if ($目录 -and -not (Test-Path $目录)) { New-Item -ItemType Directory -Path $目录 -Force | Out-Null } } # 若 media_type 与文件扩展名不一致,按实际格式调整扩展名 $media属性 = $图像.PSObject.Properties['media_type'] if ($media属性 -and $media属性.Value) { $实际扩展名 = switch ($media属性.Value) { 'image/jpeg' { '.jpg' } 'image/webp' { '.webp' } 'image/gif' { '.gif' } 'image/png' { '.png' } default { [System.IO.Path]::GetExtension($当前路径) } } if ($实际扩展名 -and [System.IO.Path]::GetExtension($当前路径) -ne $实际扩展名) { $当前路径 = [System.IO.Path]::ChangeExtension($当前路径, $实际扩展名) } } $b64属性 = $图像.PSObject.Properties['b64_json'] if (-not $b64属性 -or -not $b64属性.Value) { throw "第 $序号 张图像缺少 b64_json:$($图像 | ConvertTo-Json -Depth 5 -Compress)" } [System.IO.File]::WriteAllBytes($当前路径, [Convert]::FromBase64String($b64属性.Value)) Write-Host "图像已保存: $当前路径" -ForegroundColor Green $保存路径列表 += $当前路径 } if ($保存路径列表.Count -eq 1) { return $保存路径列表[0] } return $保存路径列表 } |