Upload browser-downloaded Suno WAVs to companion

This commit is contained in:
DESKTOP-KSVGT20\shkim
2026-05-02 00:44:40 +09:00
parent 873223d8c6
commit 9049136574
3 changed files with 144 additions and 8 deletions
+102 -6
View File
@@ -1,7 +1,7 @@
// ==UserScript==
// @name ChatGPT to Suno Prompt Copier
// @namespace local.suno-helper
// @version 0.7.9
// @version 0.8.0
// @description Copy structured ChatGPT song prompt versions into Suno fields.
// @author local
// @match https://chatgpt.com/*
@@ -28,7 +28,7 @@
(function () {
"use strict";
const SCRIPT_VERSION = "0.7.9";
const SCRIPT_VERSION = "0.8.0";
const STORAGE_KEY = "chatgptSunoPromptSlots";
const SLOT_COUNT = 2;
const isChatGPT = location.hostname.includes("chatgpt.com") || location.hostname.includes("chat.openai.com");
@@ -890,6 +890,41 @@
});
}
function callTampermonkeyBytes(url, options = {}) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: options.method || "GET",
url,
headers: options.headers || {},
anonymous: false,
timeout: 120000,
responseType: "arraybuffer",
onload: (response) => {
resolve({
ok: response.status >= 200 && response.status < 300,
status: response.status,
statusText: response.statusText || "",
finalUrl: response.finalUrl || url,
buffer: response.response,
});
},
onerror: (error) => reject(new Error(error?.error || "GM_xmlhttpRequest bytes failed")),
ontimeout: () => reject(new Error("GM_xmlhttpRequest bytes timeout")),
});
});
}
function arrayBufferToBase64(buffer) {
const bytes = new Uint8Array(buffer);
const chunkSize = 0x8000;
let binary = "";
for (let index = 0; index < bytes.length; index += chunkSize) {
const chunk = bytes.subarray(index, index + chunkSize);
binary += String.fromCharCode(...chunk);
}
return btoa(binary);
}
function extractWavUrl(value) {
if (!value) return "";
if (typeof value === "string") {
@@ -1019,6 +1054,47 @@
return extractWavUrl(track.wavUrl) || cdnWavUrl;
}
async function downloadSunoWavBuffer(track, statusTarget) {
if (!track.id) throw new Error("clip id 없음");
const base = `https://studio-api-prod.suno.com/api/gen/${encodeURIComponent(track.id)}`;
const candidates = [];
setStatus(statusTarget, `브라우저에서 WAV 변환 요청 중: ${track.title || track.id}`);
try {
await callSunoJson(`${base}/convert_wav/`, { method: "POST" });
} catch (error) {
console.warn("[ChatGPT to Suno] convert_wav failed before browser download", error);
}
for (let attempt = 0; attempt < 20; attempt += 1) {
try {
const data = await callSunoJson(`${base}/wav_file/`, { method: "GET" });
const direct = extractWavUrl(data);
if (direct) candidates.push(direct);
} catch (error) {
console.warn("[ChatGPT to Suno] wav_file probe failed before browser download", error);
}
candidates.push(`https://studio-api-prod.suno.com/api/billing/clips/${encodeURIComponent(track.id)}/download/`);
candidates.push(`https://cdn1.suno.ai/${encodeURIComponent(track.id)}.wav`);
for (const url of unique(candidates)) {
try {
setStatus(statusTarget, `브라우저에서 WAV 받는 중: ${track.title || track.id}`);
const response = await callTampermonkeyBytes(url, { method: "GET" });
const size = response.buffer?.byteLength || 0;
if (response.ok && size > 1024 * 1024) {
return response.buffer;
}
console.warn("[ChatGPT to Suno] WAV bytes not ready", url, response.status, size);
} catch (error) {
console.warn("[ChatGPT to Suno] WAV bytes download failed", url, error);
}
}
await new Promise((resolve) => setTimeout(resolve, 1500));
}
throw new Error("브라우저도 WAV 다운로드를 못 받았어");
}
function downloadTextFile(text, name) {
const blob = new Blob([text], { type: "application/json;charset=utf-8" });
const url = URL.createObjectURL(blob);
@@ -1171,6 +1247,27 @@
setStatus(statusTarget, `큐 전송 완료: 새 곡 ${enqueued}개, 보류 ${wavPending}개. 로컬 프로그램이 WAV 준비를 기다리며 처리 중.`);
}
async function uploadTrackToCompanion(track, statusTarget) {
const buffer = await downloadSunoWavBuffer(track, statusTarget);
setStatus(statusTarget, `로컬 프로그램으로 WAV 업로드 중: ${track.title || track.id}`);
const payload = {
pageUrl: location.href,
capturedAt: new Date().toISOString(),
track,
wavBase64: arrayBufferToBase64(buffer),
};
const response = await fetch("http://127.0.0.1:17873/upload-track", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(payload),
});
const text = await response.text();
if (!response.ok) {
throw new Error(text || `${response.status} ${response.statusText}`);
}
return tryParseJson(text) || {};
}
function trackOptionLabel(track, index) {
const title = track?.title || track?.id || "제목 없음";
const shortId = track?.id ? String(track.id).slice(0, 8) : "";
@@ -1319,11 +1416,10 @@
return;
}
try {
setStatus(body, `선택 1곡 처리 준비: ${track.title || track.id}`);
await postToCompanion([track], body);
const data = await uploadTrackToCompanion(track, body);
setStatus(body, `선택 1곡 업로드 완료: ${track.title || track.id} / 대기 ${data.queued ?? "?"}개 / 처리 ${data.active || "대기"}`);
} catch (error) {
setStatus(body, `로컬 프로그램 연결 실패. 선택 1곡 manifest로 저장할게: ${error.message || error}`);
await exportSunoManifest([track], body);
setStatus(body, `선택 1곡 처리 실패: ${error.message || error}`);
}
});