param( [int]$Port = 17873, [string]$OutputRoot = "\\VaultOfData\AKAMedia\음악\처리됨\SUNO", [string]$FfmpegPath = "" ) $ErrorActionPreference = "Stop" $scriptRoot = Split-Path -Parent $MyInvocation.MyCommand.Path $processor = Join-Path $scriptRoot "process-suno-library.ps1" $inbox = Join-Path $scriptRoot "companion-inbox" $logs = Join-Path $scriptRoot "companion-logs" New-Item -ItemType Directory -Force -Path $inbox, $logs | Out-Null function Write-Response { param( [System.Net.Sockets.NetworkStream]$Stream, [int]$StatusCode, [string]$StatusText, [string]$Body, [string]$ContentType = "application/json; charset=utf-8" ) $bodyBytes = [Text.Encoding]::UTF8.GetBytes($Body) $header = @( "HTTP/1.1 $StatusCode $StatusText", "Content-Type: $ContentType", "Content-Length: $($bodyBytes.Length)", "Access-Control-Allow-Origin: *", "Access-Control-Allow-Methods: GET, POST, OPTIONS", "Access-Control-Allow-Headers: content-type", "Access-Control-Allow-Private-Network: true", "Connection: close", "", "" ) -join "`r`n" $headerBytes = [Text.Encoding]::ASCII.GetBytes($header) $Stream.Write($headerBytes, 0, $headerBytes.Length) $Stream.Write($bodyBytes, 0, $bodyBytes.Length) } function Read-HttpRequest { param([System.Net.Sockets.NetworkStream]$Stream) $buffer = New-Object byte[] 65536 $received = New-Object System.Collections.Generic.List[byte] $headerEnd = -1 while ($headerEnd -lt 0) { $read = $Stream.Read($buffer, 0, $buffer.Length) if ($read -le 0) { break } for ($i = 0; $i -lt $read; $i++) { $received.Add($buffer[$i]) } $text = [Text.Encoding]::ASCII.GetString($received.ToArray()) $headerEnd = $text.IndexOf("`r`n`r`n", [StringComparison]::Ordinal) if ($received.Count -gt 1048576) { throw "HTTP header too large" } } if ($headerEnd -lt 0) { throw "Invalid HTTP request" } $allBytes = $received.ToArray() $headerText = [Text.Encoding]::ASCII.GetString($allBytes, 0, $headerEnd) $lines = $headerText -split "`r`n" $requestLine = $lines[0] -split " " $headers = @{} foreach ($line in $lines | Select-Object -Skip 1) { $idx = $line.IndexOf(":") if ($idx -gt 0) { $headers[$line.Substring(0, $idx).Trim().ToLowerInvariant()] = $line.Substring($idx + 1).Trim() } } $contentLength = 0 if ($headers.ContainsKey("content-length")) { $contentLength = [int]$headers["content-length"] } $bodyStart = $headerEnd + 4 $bodyBytes = New-Object System.Collections.Generic.List[byte] for ($i = $bodyStart; $i -lt $allBytes.Length; $i++) { $bodyBytes.Add($allBytes[$i]) } while ($bodyBytes.Count -lt $contentLength) { $read = $Stream.Read($buffer, 0, [Math]::Min($buffer.Length, $contentLength - $bodyBytes.Count)) if ($read -le 0) { break } for ($i = 0; $i -lt $read; $i++) { $bodyBytes.Add($buffer[$i]) } } return [pscustomobject]@{ Method = $requestLine[0] Path = $requestLine[1] Headers = $headers Body = [Text.Encoding]::UTF8.GetString($bodyBytes.ToArray()) } } function Start-SunoProcess { param([string]$ManifestPath) $jobId = Get-Date -Format "yyyyMMdd-HHmmss" $stdout = Join-Path $logs "$jobId.out.log" $stderr = Join-Path $logs "$jobId.err.log" $args = @( "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", $processor, "-ManifestPath", $ManifestPath, "-OutputRoot", $OutputRoot ) if ($FfmpegPath) { $args += @("-FfmpegPath", $FfmpegPath) } $process = Start-Process -FilePath "powershell.exe" -ArgumentList $args -PassThru -NoNewWindow -RedirectStandardOutput $stdout -RedirectStandardError $stderr return [pscustomobject]@{ jobId = $jobId pid = $process.Id log = $stdout errorLog = $stderr } } if (-not (Test-Path -LiteralPath $processor)) { throw "processor not found: $processor" } $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Parse("127.0.0.1"), $Port) $listener.Start() Write-Host "SUNO companion listening on http://127.0.0.1:$Port" Write-Host "OutputRoot: $OutputRoot" Write-Host "Logs: $logs" Write-Host "Press Ctrl+C to stop." try { while ($true) { $client = $listener.AcceptTcpClient() try { $stream = $client.GetStream() $request = Read-HttpRequest $stream if ($request.Method -eq "OPTIONS") { Write-Response $stream 204 "No Content" "" continue } if ($request.Method -eq "GET" -and $request.Path -eq "/health") { Write-Response $stream 200 "OK" (@{ ok = $true; outputRoot = $OutputRoot } | ConvertTo-Json -Compress) continue } if ($request.Method -eq "POST" -and $request.Path -eq "/process") { $manifest = $request.Body | ConvertFrom-Json if (-not $manifest.tracks -or @($manifest.tracks).Count -eq 0) { Write-Response $stream 400 "Bad Request" (@{ ok = $false; error = "tracks is empty" } | ConvertTo-Json -Compress) continue } $manifestPath = Join-Path $inbox ("suno-library-{0}.manifest.json" -f (Get-Date -Format "yyyyMMdd-HHmmss")) $request.Body | 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" (@{ ok = $true jobId = $job.jobId pid = $job.pid log = $job.log errorLog = $job.errorLog } | ConvertTo-Json -Compress) continue } Write-Response $stream 404 "Not Found" (@{ ok = $false; error = "not found" } | ConvertTo-Json -Compress) } catch { try { Write-Response $stream 500 "Internal Server Error" (@{ ok = $false; error = "$($_.Exception.Message)" } | ConvertTo-Json -Compress) } catch {} Write-Warning $_.Exception.Message } finally { $client.Close() } } } finally { $listener.Stop() }