Add local Suno companion service
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
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",
|
||||
"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()
|
||||
}
|
||||
Reference in New Issue
Block a user