diff --git a/chatgpt-suno-tampermonkey.user.js b/chatgpt-suno-tampermonkey.user.js index 9b95f32..15368c9 100644 --- a/chatgpt-suno-tampermonkey.user.js +++ b/chatgpt-suno-tampermonkey.user.js @@ -1,7 +1,7 @@ // ==UserScript== // @name ChatGPT to Suno Prompt Copier // @namespace local.suno-helper -// @version 0.8.6 +// @version 0.8.7 // @description Copy structured ChatGPT song prompt versions into Suno fields. // @author local // @match https://chatgpt.com/* @@ -30,7 +30,7 @@ (function () { "use strict"; - const SCRIPT_VERSION = "0.8.6"; + const SCRIPT_VERSION = "0.8.7"; const STORAGE_KEY = "chatgptSunoPromptSlots"; const SLOT_COUNT = 2; const isChatGPT = location.hostname.includes("chatgpt.com") || location.hostname.includes("chat.openai.com"); @@ -1417,7 +1417,11 @@ } 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) { @@ -1438,7 +1442,7 @@ return; } const data = tryParseJson(text) || {}; - setStatus(statusTarget, `연결 OK: ${data.outputRoot || "outputRoot 확인됨"}`); + setStatus(statusTarget, `연결 OK: ${formatCompanionStatus(data)}`); } catch (error) { setStatus(statusTarget, `연결 실패: companion을 켜고 새로고침해줘. ${error.message || error}`); } @@ -1456,6 +1460,20 @@ 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() { const body = createPanel("Suno Prompt Paste"); const slots = await readSlots(); @@ -1573,7 +1591,7 @@ } try { 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) { setStatus(body, `선택 1곡 ${format.toUpperCase()} 처리 실패: ${error.message || error}`); } diff --git a/process-suno-library.ps1 b/process-suno-library.ps1 index da7ee18..8cdeb09 100644 --- a/process-suno-library.ps1 +++ b/process-suno-library.ps1 @@ -370,7 +370,8 @@ if (-not $tracks.Count) { $libraryRoot = Resolve-LibraryRoot $OutputRoot $ArtistName 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 Write-Host "Manifest: $manifestFile" diff --git a/suno-companion.ps1 b/suno-companion.ps1 index a40de64..7088d65 100644 --- a/suno-companion.ps1 +++ b/suno-companion.ps1 @@ -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 }