Files
suno-helper/process-suno-library.ps1
T
2026-05-01 17:49:41 +09:00

263 lines
7.8 KiB
PowerShell

param(
[string]$ManifestPath = "",
[string]$OutputRoot = "\\VaultOfData\AKAMedia\음악\처리됨\SUNO",
[string]$FfmpegPath = "",
[switch]$KeepWorkFiles
)
$ErrorActionPreference = "Stop"
function Resolve-Ffmpeg {
param([string]$PreferredPath)
if ($PreferredPath -and (Test-Path -LiteralPath $PreferredPath)) {
return (Resolve-Path -LiteralPath $PreferredPath).Path
}
$repoCandidate = "E:\forfun\ffmpeg\bin\ffmpeg.exe"
if (Test-Path -LiteralPath $repoCandidate) {
return $repoCandidate
}
$cmd = Get-Command ffmpeg -ErrorAction SilentlyContinue
if ($cmd) {
return $cmd.Source
}
throw "ffmpeg.exe를 찾지 못했어. -FfmpegPath 로 경로를 지정해줘."
}
function Resolve-Manifest {
param([string]$Path)
if ($Path -and (Test-Path -LiteralPath $Path)) {
return (Resolve-Path -LiteralPath $Path).Path
}
$candidates = @()
$downloads = Join-Path $env:USERPROFILE "Downloads"
if (Test-Path -LiteralPath $downloads) {
$candidates += Get-ChildItem -LiteralPath $downloads -Filter "suno-library-*.manifest.json" -File -ErrorAction SilentlyContinue
}
$candidates += Get-ChildItem -LiteralPath (Get-Location) -Filter "suno-library-*.manifest.json" -File -ErrorAction SilentlyContinue
$latest = $candidates | Sort-Object LastWriteTime -Descending | Select-Object -First 1
if ($latest) {
return $latest.FullName
}
throw "manifest 파일을 찾지 못했어. -ManifestPath 로 suno-library-*.manifest.json 경로를 넘겨줘."
}
function ConvertTo-SafeName {
param(
[string]$Value,
[string]$Fallback = "suno-track"
)
$name = if ($Value) { $Value } else { $Fallback }
$name = $name -replace '[\\/:*?"<>|]+', ' '
$name = $name -replace '\s+', ' '
$name = $name.Trim()
if (-not $name) { $name = $Fallback }
if ($name.Length -gt 120) { $name = $name.Substring(0, 120).Trim() }
return $name
}
function Get-ObjectValues {
param($Value)
if ($null -eq $Value) { return }
if ($Value -is [System.Collections.IEnumerable] -and -not ($Value -is [string]) -and -not ($Value -is [pscustomobject])) {
foreach ($item in $Value) { Get-ObjectValues $item }
return
}
if ($Value -is [pscustomobject]) {
$Value
foreach ($prop in $Value.PSObject.Properties) {
Get-ObjectValues $prop.Value
}
}
}
function Get-DeepValue {
param(
$Object,
[string[]]$Keys
)
foreach ($node in Get-ObjectValues $Object) {
foreach ($key in $Keys) {
$prop = $node.PSObject.Properties[$key]
if ($prop -and $null -ne $prop.Value -and "$($prop.Value)".Trim()) {
return "$($prop.Value)".Trim()
}
}
}
return ""
}
function Get-UrlExtension {
param(
[string]$Url,
[string]$Fallback
)
$clean = ($Url -split '[?#]')[0]
$ext = [IO.Path]::GetExtension($clean).TrimStart(".")
if ($ext) { return $ext.ToLowerInvariant() }
return $Fallback
}
function Invoke-Download {
param(
[string]$Url,
[string]$OutFile
)
if (-not $Url) { return $false }
$headers = @{
"User-Agent" = "Mozilla/5.0"
"Referer" = "https://suno.com/"
}
try {
Invoke-WebRequest -Uri $Url -OutFile $OutFile -Headers $headers -UseBasicParsing
return (Test-Path -LiteralPath $OutFile) -and ((Get-Item -LiteralPath $OutFile).Length -gt 0)
} catch {
Write-Warning "다운로드 실패: $Url :: $($_.Exception.Message)"
return $false
}
}
function Add-MetadataArg {
param(
[System.Collections.Generic.List[string]]$Args,
[string]$Key,
[string]$Value
)
if ($Value) {
$Args.Add("-metadata")
$Args.Add("$Key=$Value")
}
}
$ffmpeg = Resolve-Ffmpeg $FfmpegPath
$manifestFile = Resolve-Manifest $ManifestPath
$manifest = Get-Content -LiteralPath $manifestFile -Raw | ConvertFrom-Json
$tracks = @($manifest.tracks)
if (-not $tracks.Count) {
throw "manifest에 tracks 배열이 없어."
}
$dateFolder = Get-Date -Format "yyyy-MM-dd"
$targetRoot = Join-Path $OutputRoot $dateFolder
$workRoot = Join-Path $targetRoot "_work"
New-Item -ItemType Directory -Force -Path $targetRoot, $workRoot | Out-Null
Write-Host "Manifest: $manifestFile"
Write-Host "Output: $targetRoot"
Write-Host "ffmpeg: $ffmpeg"
Write-Host "Tracks: $($tracks.Count)"
$index = 0
foreach ($track in $tracks) {
$index++
$id = if ($track.id) { "$($track.id)" } else { "" }
$title = if ($track.title) { "$($track.title)" } else { $id }
$safeTitle = ConvertTo-SafeName $title $id
$prefix = "{0:D3} - {1}" -f $index, $safeTitle
$lyrics = Get-DeepValue $track @("prompt", "lyrics", "lyric", "text")
$style = Get-DeepValue $track @("gpt_description_prompt", "style", "tags", "description")
$model = Get-DeepValue $track @("model_name", "model", "major_model_version")
$createdAt = Get-DeepValue $track @("created_at", "createdAt")
$wavUrl = if ($track.wavUrl) { "$($track.wavUrl)" } elseif ($id) { "https://cdn1.suno.ai/$id.wav" } else { "" }
$audioUrl = if ($track.audioUrl) { "$($track.audioUrl)" } else { "" }
$imageUrl = if ($track.imageUrl) { "$($track.imageUrl)" } else { Get-DeepValue $track @("image_url", "imageUrl", "image_large_url", "cover_url") }
$inputAudio = Join-Path $workRoot "$prefix.input.wav"
$fallbackAudio = Join-Path $workRoot "$prefix.input.$(Get-UrlExtension $audioUrl 'audio')"
$coverFile = Join-Path $workRoot "$prefix.cover.$(Get-UrlExtension $imageUrl 'jpg')"
$metadataFile = Join-Path $targetRoot "$prefix.metadata.json"
$flacFile = Join-Path $targetRoot "$prefix.flac"
Write-Host ""
Write-Host "[$index/$($tracks.Count)] $title"
$audioOk = Invoke-Download $wavUrl $inputAudio
if (-not $audioOk -and $audioUrl) {
Write-Host "WAV 후보 실패. audioUrl로 fallback 시도."
$audioOk = Invoke-Download $audioUrl $fallbackAudio
if ($audioOk) { $inputAudio = $fallbackAudio }
}
if (-not $audioOk) {
Write-Warning "오디오를 받지 못해서 건너뜀: $title"
continue
}
$coverOk = $false
if ($imageUrl) {
$coverOk = Invoke-Download $imageUrl $coverFile
}
$track | ConvertTo-Json -Depth 50 | Set-Content -LiteralPath $metadataFile -Encoding UTF8
$argsList = [System.Collections.Generic.List[string]]::new()
$argsList.Add("-hide_banner")
$argsList.Add("-y")
$argsList.Add("-i")
$argsList.Add($inputAudio)
if ($coverOk) {
$argsList.Add("-i")
$argsList.Add($coverFile)
$argsList.Add("-map")
$argsList.Add("0:a")
$argsList.Add("-map")
$argsList.Add("1:v")
$argsList.Add("-disposition:v")
$argsList.Add("attached_pic")
$argsList.Add("-c:v")
$argsList.Add("copy")
}
$argsList.Add("-c:a")
$argsList.Add("flac")
$argsList.Add("-compression_level")
$argsList.Add("8")
Add-MetadataArg $argsList "title" $title
Add-MetadataArg $argsList "artist" "SUNO"
Add-MetadataArg $argsList "album" "SUNO"
Add-MetadataArg $argsList "genre" $style
Add-MetadataArg $argsList "lyrics" $lyrics
Add-MetadataArg $argsList "description" $style
Add-MetadataArg $argsList "comment" "Suno clip id: $id"
Add-MetadataArg $argsList "date" $createdAt
Add-MetadataArg $argsList "encoder" "Codex SUNO helper"
Add-MetadataArg $argsList "suno_model" $model
$argsList.Add($flacFile)
& $ffmpeg @argsList
if ($LASTEXITCODE -ne 0) {
Write-Warning "FLAC 변환 실패: $title"
continue
}
Write-Host "완료: $flacFile"
}
if (-not $KeepWorkFiles -and (Test-Path -LiteralPath $workRoot)) {
Remove-Item -LiteralPath $workRoot -Recurse -Force
}
Write-Host ""
Write-Host "처리 완료: $targetRoot"