Add parallel companion processing dashboard

This commit is contained in:
DESKTOP-KSVGT20\shkim
2026-05-02 01:31:25 +09:00
parent 0d03921b1a
commit 056a08a5c8
3 changed files with 255 additions and 71 deletions
+230 -65
View File
@@ -2,7 +2,8 @@ param(
[int]$Port = 17873,
[string]$OutputRoot = "\\VaultOfData\AKAMedia\음악\처리됨",
[string]$ArtistName = "SUNO",
[string]$FfmpegPath = ""
[string]$FfmpegPath = "",
[int]$MaxWorkers = 3
)
$ErrorActionPreference = "Stop"
@@ -14,9 +15,17 @@ $logs = Join-Path $scriptRoot "companion-logs"
New-Item -ItemType Directory -Force -Path $inbox, $logs | Out-Null
$queue = [System.Collections.Queue]::new()
$queueIds = [System.Collections.Generic.HashSet[string]]::new()
$activeJob = $null
$activeJobs = [System.Collections.ArrayList]::new()
$activeIds = [System.Collections.Generic.HashSet[string]]::new()
$stats = [ordered]@{
total = 0
completed = 0
failed = 0
skipped = 0
}
$lastProgressAt = [DateTime]::MinValue
$lastProgressText = ""
$lastDashboardAt = [DateTime]::MinValue
function Resolve-LibraryRoot {
param(
@@ -118,9 +127,13 @@ function Read-HttpRequest {
}
function Start-SunoProcess {
param([string]$ManifestPath)
param(
[string]$ManifestPath,
[string]$TrackId
)
$jobId = Get-Date -Format "yyyyMMdd-HHmmss"
$shortId = if ($TrackId) { "$TrackId".Substring(0, [Math]::Min(8, "$TrackId".Length)) } else { "track" }
$jobId = "{0}-{1}" -f (Get-Date -Format "yyyyMMdd-HHmmss-fff"), $shortId
$stdout = Join-Path $logs "$jobId.out.log"
$stderr = Join-Path $logs "$jobId.err.log"
$args = @(
@@ -158,41 +171,6 @@ function Save-Manifest {
return $manifestPath
}
function Update-Queue {
if ($script:activeJob -and -not $script:activeJob.Process.HasExited) {
Publish-ActiveJobProgress
return
}
if ($script:activeJob -and $script:activeJob.Process.HasExited) {
Publish-ActiveJobProgress -Force
Write-Host "Finished job $($script:activeJob.JobId) exit=$($script:activeJob.Process.ExitCode)"
$script:activeJob = $null
}
while (-not $script:activeJob -and $script:queue.Count -gt 0) {
$item = $script:queue.Dequeue()
[void]$script:queueIds.Remove($item.Id)
$manifest = [pscustomobject]@{
pageUrl = $item.PageUrl
capturedAt = (Get-Date).ToString("o")
count = 1
tracks = @($item.Track)
}
$manifestPath = Save-Manifest $manifest
$job = Start-SunoProcess $manifestPath
$script:activeJob = [pscustomobject]@{
JobId = $job.jobId
Process = $job.process
TrackId = $item.Id
Title = $item.Title
Log = $job.log
ErrorLog = $job.errorLog
}
Write-Host "Started queued job $($job.jobId): $($item.Title) [$($item.Id)]"
}
}
function Get-LastLogLine {
param([string[]]$Paths)
@@ -206,20 +184,158 @@ function Get-LastLogLine {
return ""
}
function Publish-ActiveJobProgress {
param([switch]$Force)
function Get-JobLine {
param($Job)
if (-not $script:activeJob) { return }
if (-not $Job) { return "" }
return Get-LastLogLine @($Job.ErrorLog, $Job.Log)
}
function Publish-JobProgress {
param($Job, [switch]$Force)
if (-not $Job) { return }
$now = Get-Date
if (-not $Force -and ($now - $script:lastProgressAt).TotalSeconds -lt 10) { return }
$line = Get-LastLogLine @($script:activeJob.ErrorLog, $script:activeJob.Log)
$line = Get-JobLine $Job
if (-not $line) { return }
if (-not $Force -and $line -eq $script:lastProgressText) { return }
$fingerprint = "$($Job.JobId):$line"
if (-not $Force -and $fingerprint -eq $script:lastProgressText) { return }
$script:lastProgressAt = $now
$script:lastProgressText = $line
Write-Host "[$($script:activeJob.JobId)] $($script:activeJob.Title): $line"
$script:lastProgressText = $fingerprint
Write-Host "[$($Job.JobId)] $($Job.Title): $line"
}
function Get-JobSnapshot {
param($Job)
$elapsed = [int]((Get-Date) - $Job.StartedAt).TotalSeconds
return [pscustomobject]@{
jobId = $Job.JobId
pid = $Job.Process.Id
trackId = $Job.TrackId
title = $Job.Title
elapsedSeconds = $elapsed
log = Get-JobLine $Job
exitCode = if ($Job.Process.HasExited) { $Job.Process.ExitCode } else { $null }
}
}
function Get-QueueSnapshot {
$active = @($script:activeJobs | ForEach-Object { Get-JobSnapshot $_ })
$total = [int]$script:stats.total
$done = [int]$script:stats.completed + [int]$script:stats.failed
$percent = if ($total -gt 0) { [Math]::Round(($done / $total) * 100, 1) } else { 100 }
$next = @($script:queue | Select-Object -First 5 | ForEach-Object {
[pscustomobject]@{
id = $_.Id
title = $_.Title
}
})
return [pscustomobject]@{
maxWorkers = $MaxWorkers
queued = $script:queue.Count
activeCount = $active.Count
active = if ($active.Count) { ($active | Select-Object -First 1).title } else { "" }
activeJobs = $active
next = $next
total = $total
completed = [int]$script:stats.completed
failed = [int]$script:stats.failed
skipped = [int]$script:stats.skipped
done = $done
percent = $percent
}
}
function Show-Dashboard {
param([switch]$Force)
$now = Get-Date
if (-not $Force -and ($now - $script:lastDashboardAt).TotalSeconds -lt 2) { return }
$script:lastDashboardAt = $now
$snapshot = Get-QueueSnapshot
Clear-Host
Write-Host "SUNO Companion" -ForegroundColor Cyan
Write-Host "Listening: http://127.0.0.1:$Port"
Write-Host "Library: $libraryRoot"
Write-Host "Logs: $logs"
Write-Host ""
Write-Host ("Workers: {0}/{1} Queue: {2} Done: {3}/{4} Failed: {5} Progress: {6}%" -f $snapshot.activeCount, $snapshot.maxWorkers, $snapshot.queued, $snapshot.done, $snapshot.total, $snapshot.failed, $snapshot.percent)
Write-Host ""
if ($snapshot.activeJobs.Count) {
Write-Host "Active jobs:" -ForegroundColor Yellow
foreach ($job in $snapshot.activeJobs) {
$elapsed = [TimeSpan]::FromSeconds($job.elapsedSeconds)
Write-Host ("- {0} [{1:mm\:ss}] pid={2}" -f $job.title, $elapsed, $job.pid)
if ($job.log) { Write-Host " $($job.log)" -ForegroundColor DarkGray }
}
} else {
Write-Host "Active jobs: none" -ForegroundColor DarkGray
}
if ($snapshot.next.Count) {
Write-Host ""
Write-Host "Next queue:" -ForegroundColor Yellow
foreach ($item in $snapshot.next) {
Write-Host "- $($item.title)"
}
}
Write-Host ""
Write-Host "Close this window to stop the companion."
}
function Update-Queue {
for ($index = $script:activeJobs.Count - 1; $index -ge 0; $index--) {
$job = $script:activeJobs[$index]
if (-not $job.Process.HasExited) {
Publish-JobProgress $job
continue
}
Publish-JobProgress $job -Force
if ($job.Process.ExitCode -eq 0) {
$script:stats.completed++
} else {
$script:stats.failed++
}
[void]$script:activeIds.Remove($job.TrackId)
$script:activeJobs.RemoveAt($index)
Write-Host "Finished job $($job.JobId) exit=$($job.Process.ExitCode): $($job.Title)"
}
while ($script:activeJobs.Count -lt $MaxWorkers -and $script:queue.Count -gt 0) {
$item = $script:queue.Dequeue()
[void]$script:queueIds.Remove($item.Id)
$manifest = [pscustomobject]@{
pageUrl = $item.PageUrl
capturedAt = (Get-Date).ToString("o")
count = 1
tracks = @($item.Track)
}
$manifestPath = Save-Manifest $manifest
$job = Start-SunoProcess $manifestPath $item.Id
$active = [pscustomobject]@{
JobId = $job.jobId
Process = $job.process
TrackId = $item.Id
Title = $item.Title
Log = $job.log
ErrorLog = $job.errorLog
StartedAt = Get-Date
}
[void]$script:activeJobs.Add($active)
[void]$script:activeIds.Add($item.Id)
Write-Host "Started queued job $($job.jobId): $($item.Title) [$($item.Id)]"
}
Show-Dashboard
}
function Add-TracksToQueue {
@@ -232,7 +348,7 @@ function Add-TracksToQueue {
$id = "$($track.id)".Trim()
if (-not $id) { $skipped++; continue }
if ($queueIds.Contains($id)) { $skipped++; continue }
if ($activeJob -and $activeJob.TrackId -eq $id) { $skipped++; continue }
if ($activeIds.Contains($id)) { $skipped++; continue }
$title = if ($track.title) { "$($track.title)" } else { $id }
$queue.Enqueue([pscustomobject]@{
Id = $id
@@ -243,12 +359,21 @@ function Add-TracksToQueue {
[void]$queueIds.Add($id)
$added += $track
}
$script:stats.total += @($added).Count
$script:stats.skipped += $skipped
Update-Queue
$snapshot = Get-QueueSnapshot
return [pscustomobject]@{
added = @($added).Count
skipped = $skipped
queued = $queue.Count
active = if ($activeJob) { $activeJob.Title } else { "" }
queued = $snapshot.queued
active = $snapshot.active
activeCount = $snapshot.activeCount
maxWorkers = $snapshot.maxWorkers
completed = $snapshot.completed
failed = $snapshot.failed
total = $snapshot.total
percent = $snapshot.percent
}
}
@@ -325,7 +450,9 @@ Write-Host "SUNO companion listening on http://127.0.0.1:$Port"
Write-Host "OutputRoot: $OutputRoot"
Write-Host "Library: $libraryRoot"
Write-Host "Logs: $logs"
Write-Host "Workers: $MaxWorkers"
Write-Host "Press Ctrl+C to stop."
Show-Dashboard -Force
try {
while ($true) {
@@ -346,8 +473,26 @@ try {
if ($request.Method -eq "GET" -and $request.Path -eq "/health") {
Update-Queue
Write-Host "Health check OK: $(Get-Date -Format 'HH:mm:ss')"
Write-Response $stream 200 "OK" (@{ ok = $true; outputRoot = $OutputRoot; libraryRoot = $libraryRoot; artist = $ArtistName; queued = $queue.Count; active = if ($activeJob) { $activeJob.Title } else { "" } } | ConvertTo-Json -Compress)
$snapshot = Get-QueueSnapshot
$payload = [ordered]@{
ok = $true
outputRoot = $OutputRoot
libraryRoot = $libraryRoot
artist = $ArtistName
queued = $snapshot.queued
active = $snapshot.active
activeCount = $snapshot.activeCount
activeJobs = $snapshot.activeJobs
next = $snapshot.next
maxWorkers = $snapshot.maxWorkers
total = $snapshot.total
completed = $snapshot.completed
failed = $snapshot.failed
skipped = $snapshot.skipped
done = $snapshot.done
percent = $snapshot.percent
}
Write-Response $stream 200 "OK" ($payload | ConvertTo-Json -Depth 10 -Compress)
continue
}
@@ -375,30 +520,38 @@ try {
continue
}
$sync = Get-MissingTracks $manifest.tracks
$manifest.tracks = @($sync.missing)
$manifest.count = @($manifest.tracks).Count
if ($manifest.count -eq 0) {
$result = Add-TracksToQueue @($manifest.tracks) "$($manifest.pageUrl)"
if ($result.added -eq 0) {
Write-Host "Process skipped: library already synced"
Write-Response $stream 200 "OK" (@{
ok = $true
skipped = $true
message = "library already synced"
message = "library already synced or already queued"
queued = $result.queued
active = $result.active
activeCount = $result.activeCount
maxWorkers = $result.maxWorkers
completed = $result.completed
failed = $result.failed
total = $result.total
percent = $result.percent
} | ConvertTo-Json -Compress)
continue
}
$manifestPath = Join-Path $inbox ("suno-library-{0}.manifest.json" -f (Get-Date -Format "yyyyMMdd-HHmmss"))
$manifest | ConvertTo-Json -Depth 50 | Set-Content -LiteralPath $manifestPath -Encoding UTF8
$job = Start-SunoProcess $manifestPath
Write-Host "Started job $($job.jobId) for $(@($manifest.tracks).Count) tracks"
Write-Host "Queued process request: added=$($result.added), queued=$($result.queued), active=$($result.activeCount)"
Write-Response $stream 200 "OK" (@{
ok = $true
jobId = $job.jobId
pid = $job.pid
log = $job.log
errorLog = $job.errorLog
added = $result.added
skipped = $result.skipped
queued = $result.queued
active = $result.active
activeCount = $result.activeCount
maxWorkers = $result.maxWorkers
completed = $result.completed
failed = $result.failed
total = $result.total
percent = $result.percent
} | ConvertTo-Json -Compress)
continue
}
@@ -417,6 +570,12 @@ try {
skipped = $result.skipped
queued = $result.queued
active = $result.active
activeCount = $result.activeCount
maxWorkers = $result.maxWorkers
completed = $result.completed
failed = $result.failed
total = $result.total
percent = $result.percent
} | ConvertTo-Json -Compress)
continue
}
@@ -431,6 +590,12 @@ try {
skipped = $result.skipped
queued = $result.queued
active = $result.active
activeCount = $result.activeCount
maxWorkers = $result.maxWorkers
completed = $result.completed
failed = $result.failed
total = $result.total
percent = $result.percent
} | ConvertTo-Json -Compress)
continue
}