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
+23 -5
View File
@@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name ChatGPT to Suno Prompt Copier // @name ChatGPT to Suno Prompt Copier
// @namespace local.suno-helper // @namespace local.suno-helper
// @version 0.8.6 // @version 0.8.7
// @description Copy structured ChatGPT song prompt versions into Suno fields. // @description Copy structured ChatGPT song prompt versions into Suno fields.
// @author local // @author local
// @match https://chatgpt.com/* // @match https://chatgpt.com/*
@@ -30,7 +30,7 @@
(function () { (function () {
"use strict"; "use strict";
const SCRIPT_VERSION = "0.8.6"; const SCRIPT_VERSION = "0.8.7";
const STORAGE_KEY = "chatgptSunoPromptSlots"; const STORAGE_KEY = "chatgptSunoPromptSlots";
const SLOT_COUNT = 2; const SLOT_COUNT = 2;
const isChatGPT = location.hostname.includes("chatgpt.com") || location.hostname.includes("chat.openai.com"); const isChatGPT = location.hostname.includes("chatgpt.com") || location.hostname.includes("chat.openai.com");
@@ -1417,7 +1417,11 @@
} }
await new Promise((resolve) => setTimeout(resolve, 400)); await new Promise((resolve) => setTimeout(resolve, 400));
} }
setStatus(statusTarget, `${format.toUpperCase()} 일괄 업로드 완료: ${uploaded}곡 성공, ${failed}곡 실패. 로컬에서 메타데이터 후처리 중.`); let healthText = "";
try {
healthText = ` / ${formatCompanionStatus(await readCompanionHealth())}`;
} catch {}
setStatus(statusTarget, `${format.toUpperCase()} 일괄 업로드 완료: ${uploaded}곡 성공, ${failed}곡 실패. 로컬에서 메타데이터 후처리 중${healthText}`);
} }
function trackOptionLabel(track, index) { function trackOptionLabel(track, index) {
@@ -1438,7 +1442,7 @@
return; return;
} }
const data = tryParseJson(text) || {}; const data = tryParseJson(text) || {};
setStatus(statusTarget, `연결 OK: ${data.outputRoot || "outputRoot 확인됨"}`); setStatus(statusTarget, `연결 OK: ${formatCompanionStatus(data)}`);
} catch (error) { } catch (error) {
setStatus(statusTarget, `연결 실패: companion을 켜고 새로고침해줘. ${error.message || error}`); setStatus(statusTarget, `연결 실패: companion을 켜고 새로고침해줘. ${error.message || error}`);
} }
@@ -1456,6 +1460,20 @@
return tryParseJson(text) || {}; return tryParseJson(text) || {};
} }
function formatCompanionStatus(data) {
if (!data || typeof data !== "object") return "상태 정보 없음";
const active = Number(data.activeCount || 0);
const queued = Number(data.queued || 0);
const done = Number(data.done ?? ((data.completed || 0) + (data.failed || 0)));
const total = Number(data.total || 0);
const failed = Number(data.failed || 0);
const percent = Number.isFinite(Number(data.percent)) ? Number(data.percent) : (total ? Math.round((done / total) * 1000) / 10 : 100);
const workerText = data.maxWorkers ? `${active}/${data.maxWorkers}` : `${active}`;
const title = data.active || "";
const titleText = title ? ` / 처리중: ${title}` : "";
return `작업 ${done}/${total} (${percent}%), 실행 ${workerText}, 대기 ${queued}, 실패 ${failed}${titleText}`;
}
async function bootSuno() { async function bootSuno() {
const body = createPanel("Suno Prompt Paste"); const body = createPanel("Suno Prompt Paste");
const slots = await readSlots(); const slots = await readSlots();
@@ -1573,7 +1591,7 @@
} }
try { try {
const data = await uploadTrackToCompanion(track, body, format); const data = await uploadTrackToCompanion(track, body, format);
setStatus(body, `선택 1곡 ${format.toUpperCase()} 업로드 완료: ${track.title || track.id} / 대기 ${data.queued ?? "?"}개 / 처리 ${data.active || "대기"}`); setStatus(body, `선택 1곡 ${format.toUpperCase()} 업로드 완료: ${track.title || track.id} / ${formatCompanionStatus(data)}`);
} catch (error) { } catch (error) {
setStatus(body, `선택 1곡 ${format.toUpperCase()} 처리 실패: ${error.message || error}`); setStatus(body, `선택 1곡 ${format.toUpperCase()} 처리 실패: ${error.message || error}`);
} }
+2 -1
View File
@@ -370,7 +370,8 @@ if (-not $tracks.Count) {
$libraryRoot = Resolve-LibraryRoot $OutputRoot $ArtistName $libraryRoot = Resolve-LibraryRoot $OutputRoot $ArtistName
New-Item -ItemType Directory -Force -Path $libraryRoot | Out-Null New-Item -ItemType Directory -Force -Path $libraryRoot | Out-Null
$workRoot = Join-Path $libraryRoot "_work" $workRootName = ConvertTo-SafeName ([IO.Path]::GetFileNameWithoutExtension($manifestFile)) "work"
$workRoot = Join-Path (Join-Path $libraryRoot "_work") $workRootName
New-Item -ItemType Directory -Force -Path $workRoot | Out-Null New-Item -ItemType Directory -Force -Path $workRoot | Out-Null
Write-Host "Manifest: $manifestFile" Write-Host "Manifest: $manifestFile"
+230 -65
View File
@@ -2,7 +2,8 @@ param(
[int]$Port = 17873, [int]$Port = 17873,
[string]$OutputRoot = "\\VaultOfData\AKAMedia\음악\처리됨", [string]$OutputRoot = "\\VaultOfData\AKAMedia\음악\처리됨",
[string]$ArtistName = "SUNO", [string]$ArtistName = "SUNO",
[string]$FfmpegPath = "" [string]$FfmpegPath = "",
[int]$MaxWorkers = 3
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
@@ -14,9 +15,17 @@ $logs = Join-Path $scriptRoot "companion-logs"
New-Item -ItemType Directory -Force -Path $inbox, $logs | Out-Null New-Item -ItemType Directory -Force -Path $inbox, $logs | Out-Null
$queue = [System.Collections.Queue]::new() $queue = [System.Collections.Queue]::new()
$queueIds = [System.Collections.Generic.HashSet[string]]::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 $lastProgressAt = [DateTime]::MinValue
$lastProgressText = "" $lastProgressText = ""
$lastDashboardAt = [DateTime]::MinValue
function Resolve-LibraryRoot { function Resolve-LibraryRoot {
param( param(
@@ -118,9 +127,13 @@ function Read-HttpRequest {
} }
function Start-SunoProcess { 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" $stdout = Join-Path $logs "$jobId.out.log"
$stderr = Join-Path $logs "$jobId.err.log" $stderr = Join-Path $logs "$jobId.err.log"
$args = @( $args = @(
@@ -158,41 +171,6 @@ function Save-Manifest {
return $manifestPath 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 { function Get-LastLogLine {
param([string[]]$Paths) param([string[]]$Paths)
@@ -206,20 +184,158 @@ function Get-LastLogLine {
return "" return ""
} }
function Publish-ActiveJobProgress { function Get-JobLine {
param([switch]$Force) 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 $now = Get-Date
if (-not $Force -and ($now - $script:lastProgressAt).TotalSeconds -lt 10) { return } 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 $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:lastProgressAt = $now
$script:lastProgressText = $line $script:lastProgressText = $fingerprint
Write-Host "[$($script:activeJob.JobId)] $($script:activeJob.Title): $line" 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 { function Add-TracksToQueue {
@@ -232,7 +348,7 @@ function Add-TracksToQueue {
$id = "$($track.id)".Trim() $id = "$($track.id)".Trim()
if (-not $id) { $skipped++; continue } if (-not $id) { $skipped++; continue }
if ($queueIds.Contains($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 } $title = if ($track.title) { "$($track.title)" } else { $id }
$queue.Enqueue([pscustomobject]@{ $queue.Enqueue([pscustomobject]@{
Id = $id Id = $id
@@ -243,12 +359,21 @@ function Add-TracksToQueue {
[void]$queueIds.Add($id) [void]$queueIds.Add($id)
$added += $track $added += $track
} }
$script:stats.total += @($added).Count
$script:stats.skipped += $skipped
Update-Queue Update-Queue
$snapshot = Get-QueueSnapshot
return [pscustomobject]@{ return [pscustomobject]@{
added = @($added).Count added = @($added).Count
skipped = $skipped skipped = $skipped
queued = $queue.Count queued = $snapshot.queued
active = if ($activeJob) { $activeJob.Title } else { "" } 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 "OutputRoot: $OutputRoot"
Write-Host "Library: $libraryRoot" Write-Host "Library: $libraryRoot"
Write-Host "Logs: $logs" Write-Host "Logs: $logs"
Write-Host "Workers: $MaxWorkers"
Write-Host "Press Ctrl+C to stop." Write-Host "Press Ctrl+C to stop."
Show-Dashboard -Force
try { try {
while ($true) { while ($true) {
@@ -346,8 +473,26 @@ try {
if ($request.Method -eq "GET" -and $request.Path -eq "/health") { if ($request.Method -eq "GET" -and $request.Path -eq "/health") {
Update-Queue Update-Queue
Write-Host "Health check OK: $(Get-Date -Format 'HH:mm:ss')" $snapshot = Get-QueueSnapshot
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) $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 continue
} }
@@ -375,30 +520,38 @@ try {
continue continue
} }
$sync = Get-MissingTracks $manifest.tracks $result = Add-TracksToQueue @($manifest.tracks) "$($manifest.pageUrl)"
$manifest.tracks = @($sync.missing) if ($result.added -eq 0) {
$manifest.count = @($manifest.tracks).Count
if ($manifest.count -eq 0) {
Write-Host "Process skipped: library already synced" Write-Host "Process skipped: library already synced"
Write-Response $stream 200 "OK" (@{ Write-Response $stream 200 "OK" (@{
ok = $true ok = $true
skipped = $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) } | ConvertTo-Json -Compress)
continue continue
} }
$manifestPath = Join-Path $inbox ("suno-library-{0}.manifest.json" -f (Get-Date -Format "yyyyMMdd-HHmmss")) Write-Host "Queued process request: added=$($result.added), queued=$($result.queued), active=$($result.activeCount)"
$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-Response $stream 200 "OK" (@{ Write-Response $stream 200 "OK" (@{
ok = $true ok = $true
jobId = $job.jobId added = $result.added
pid = $job.pid skipped = $result.skipped
log = $job.log queued = $result.queued
errorLog = $job.errorLog active = $result.active
activeCount = $result.activeCount
maxWorkers = $result.maxWorkers
completed = $result.completed
failed = $result.failed
total = $result.total
percent = $result.percent
} | ConvertTo-Json -Compress) } | ConvertTo-Json -Compress)
continue continue
} }
@@ -417,6 +570,12 @@ try {
skipped = $result.skipped skipped = $result.skipped
queued = $result.queued queued = $result.queued
active = $result.active active = $result.active
activeCount = $result.activeCount
maxWorkers = $result.maxWorkers
completed = $result.completed
failed = $result.failed
total = $result.total
percent = $result.percent
} | ConvertTo-Json -Compress) } | ConvertTo-Json -Compress)
continue continue
} }
@@ -431,6 +590,12 @@ try {
skipped = $result.skipped skipped = $result.skipped
queued = $result.queued queued = $result.queued
active = $result.active active = $result.active
activeCount = $result.activeCount
maxWorkers = $result.maxWorkers
completed = $result.completed
failed = $result.failed
total = $result.total
percent = $result.percent
} | ConvertTo-Json -Compress) } | ConvertTo-Json -Compress)
continue continue
} }