get music timestamps

This commit is contained in:
2025-09-03 09:52:52 +02:00
parent 289db21e53
commit 4cabe4df3c
8 changed files with 228 additions and 43 deletions
-6
View File
@@ -42,12 +42,6 @@ jspm_packages/
# Firebase
.firebase/
# Service account key
serviceAccountKey.json
# API keys
config/keys.js
# Build files
dist/
build/
+3
View File
@@ -0,0 +1,3 @@
exports.GEMINI_API_KEY = "AIzaSyBoPQC5ZaMKP73TlKGpZQp1mAw8ArHiH9Y";
// exports.SUNO_API_KEY = "543b5650b30d93e98d8046a830244c80"; client api key
exports.SUNO_API_KEY = "138bace8405b869f917156440acb6fed"; // perso api key
+13
View File
@@ -0,0 +1,13 @@
{
"type": "service_account",
"project_id": "musicland-d33f9",
"private_key_id": "c1110a2aa31eb3ac7cf42d931249a2572bf68e37",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCZKlW0nTIg4Il2\nPtr/BYi247mWp8pxpCwS2ggwGBU7iO44jLQAgLWtZdHrQ7kpRfjVuceJDrkLrS82\ntkt8wQpytCFKfZjil9fdyS1hrVIQdfaPppovbRtzDpe0varA8XxgWXG8L+CQtCQd\nNhcixKONFhgOWTKB/tvEtGEExtpRRTb7y21o/EFbfpZqSbGEPcota2DPPvNh1r1y\nnhF7AcboGFTouDaTVL/oFIYYm3eWQP6nJRiM15xPqEhar+2z3lG9Y6HoRkkynFFj\nwvhQ6AUidA4A1ZW90HC33jsY5u7MAbzXY/QZ2MC82gaf26rda/7hThmk0RUSEaOv\n/G2f2lQBAgMBAAECggEAKnssdEDZQ3cjiTFbyjm9toQo2FlHdAeF1eC1PqmfUfjj\nobmcvzrzqNlBxM/uzjzDDHxPlHsJ5ewt2S3sd/iOrku5pBB70/UrgS8oMBxDU590\nOFmMEDVh1KKqfw3QC12vEoYcXoak8V2VWqjC9/i0KBCFsY0ZziZizAb/lbSVns52\nuJQhjvZURHQQOQmrZDm5mhklKCjE6qDxAMjL0yIhDQLAATzqeVPPeP8Rvq6u7yiB\noiB40OGiGHLdt28Q7IAXSQ+/Vqig7aw/qwrfZrFqgbgCYJKo1GiYFaDgRbH6GiaE\nhBJg7LtM0EaDR5G0raBWv5szqpfOb2DC6K802dcQRQKBgQDQm7HoJjmvEu9a6hxF\nVwS3rdJO67dTtxFMZM0HespQu659NT8f21QsRf/2JIcwktTnGzSeVTca0GZ4z8de\nkP9Wid0VPcA6Y3U7yplBf7Xh/5KW7/O6iSCEN03Y3/lH6vK6tBe5xCT3kfYKkrAd\n/xePLLBx++dAL5YFixMzBFfI5wKBgQC79i2S6Bcu6WQc0yykVHkAMStPz4gsAYA5\nqqCuWEwFcgEEY5AiP/oH056Hu2Z2HJHPp4OKYWSbqdxkNM214vysO0yhrF59BjQX\nyo+hwQfBqS0JfH7bJgUjNuDjP947qGtRXVo2i3FRJuZ4NmS2cGC9gT00p6rZRrbL\nJkaKSuVW1wKBgH3xsgxVn7fqd6Xs99qJOejnQbSyLUGWohP8O6jLYoAxf5xKXG0Q\nTxZXwLxLu28BBxbsXu1Ex3sh3OwNjfGzGuWVYGvllzvpNis+Gx7nStlCIq+L+Rti\nlMYeWZqZRng8CgXcmwJlwGfTsLcbbsb9bqzbLz/x9ODUmf+IwrOnaJR7AoGBALA7\nPjm6YFo8vAAGw5mEFjtm3owjMjLIaXgoEiXFZenJHUkHLNHgGEcUVFRBEe12FgST\ntFKfxFvrJFfT5ng+TdgSdWQ0oBcrJNaVtpvbx7D9NNvrHWnOOQzuXGbVsEFRROXx\n2C7CCUokZfyaS01/CkVEEqX2+spjceoBhiwN0v/bAoGBAK4sYw+3o3yjH26/M/Ni\nn5hwxofTktvAkYi7GlSFaRf2QwwxMBeA6qM/AJn7rer6pRzWwApbae/kOyfZfivq\nLnfpwloJMtds/Mm2egz0zoX7WIZ15uEx4vha2IUmi4Qz92kUzgaD1jTN8ZLUCypn\npR/zlJVkLv5BFqOtfX8shz1T\n-----END PRIVATE KEY-----\n",
"client_email": "firebase-adminsdk-fbsvc@musicland-d33f9.iam.gserviceaccount.com",
"client_id": "114560176426659117925",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40musicland-d33f9.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}
+4 -4
View File
@@ -69,19 +69,19 @@ Pour chaque ` +
.string()
.describe(
'Type de section : "couplet" ou "refrain" ' +
"selon la structure",
"selon la structure"
),
lyrics: z
.string()
.describe(
"Paroles de la section, chaque ligne séparée " +
"par un retour à la ligne",
"par un retour à la ligne"
),
}),
})
)
.describe(
"Paroles de la chanson sous forme de tableau de sections " +
"structurées.",
"structurées."
),
success: z.boolean().describe("Indique si la génération a réussi"),
});
+137 -12
View File
@@ -13,6 +13,7 @@ const db = admin.firestore();
const SUNO_API_BASE = "https://api.sunoapi.org";
const SUNO_API_PATH = "/api/v1/generate";
const SUNO_STATUS_PATH = "/api/v1/generate/record-info";
const SUNO_TIMESTAMPED_LYRICS_PATH = "/api/v1/generate/get-timestamped-lyrics";
const SUNO_MODEL = "V4_5";
// const SUNO_MODEL = "V3_5";
const SUNO_CALLBACK_URL =
@@ -199,7 +200,7 @@ exports.generateMusic = onCall(async ({ data = {} }) => {
"Content-Type": "application/json",
Authorization: `Bearer ${SUNO_API_KEY}`,
},
},
}
);
const parsed = response.data;
@@ -218,7 +219,7 @@ exports.generateMusic = onCall(async ({ data = {} }) => {
} catch (error) {
console.error("❌ Erreur lors de la génération de musique:", error);
throw new Error(
`Erreur lors de la génération de musique: ${error.message}`,
`Erreur lors de la génération de musique: ${error.message}`
);
}
});
@@ -247,7 +248,7 @@ exports.getSunoStatus = onCall(async ({ data = {} }) => {
Authorization: `Bearer ${SUNO_API_KEY}`,
},
timeout: 30000, // 30 secondes de timeout
},
}
);
parsed = response.data;
@@ -324,6 +325,131 @@ exports.getSunoStatus = onCall(async ({ data = {} }) => {
}
});
/**
* Récupère les timestamps (aligned words) pour une génération Suno
* Attend: { taskId: string, musicIndex: number, projectId: string }
*/
exports.getSunoTimestamps = onCall(async ({ data = {} }) => {
try {
const { taskId, musicIndex, projectId } = data || {};
if (!taskId) {
throw new Error("TaskId manquant");
}
const index = Number(musicIndex);
if (!Number.isFinite(index) || index < 0) {
throw new Error("musicIndex invalide");
}
if (!projectId || typeof projectId !== "string" || !projectId.trim()) {
throw new Error("projectId manquant ou invalide");
}
console.log("🔎 Récupération des timestamps Suno", {
taskId,
musicIndex: index,
});
let response;
let parsed;
try {
response = await axios.post(
`${SUNO_API_BASE}${SUNO_TIMESTAMPED_LYRICS_PATH}`,
{ taskId, musicIndex: index },
{
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${SUNO_API_KEY}`,
},
timeout: 30000,
}
);
parsed = response.data;
console.log("📊 Réponse timestamps Suno API:", parsed?.code, parsed?.msg);
} catch (error) {
console.error("❌ Erreur API Suno Timestamps:", {
message: error.message,
status: error.response?.status,
statusText: error.response?.statusText,
data: error.response?.data,
config: {
url: error.config?.url,
headers: error.config?.headers
? { ...error.config.headers, Authorization: "[redacted]" }
: undefined,
},
});
const errorStatus = error.response?.status || "UNKNOWN";
const errorMessage =
error.response?.data?.msg ||
error.response?.data?.message ||
error.response?.statusText ||
error.message;
return {
success: false,
taskId,
musicIndex: index,
error: {
status: errorStatus,
message: errorMessage,
type: "API_ERROR",
},
};
}
const dataToReturn = parsed?.data || parsed || {};
// Sauvegarde obligatoire dans le document projet
let saved = false;
try {
await db
.collection("projects")
.doc(projectId)
.set(
{
[`musicTimestamps.${index}`]: dataToReturn,
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
},
{ merge: true }
);
saved = true;
console.log("💾 Timestamps sauvegardés dans Firestore", {
projectId,
index,
});
} catch (e) {
console.error(
"❌ Échec sauvegarde timestamps Firestore:",
e?.message || e
);
}
return {
success: true,
taskId,
musicIndex: index,
projectId,
saved,
musicTimestamps: dataToReturn,
};
} catch (error) {
console.error("❌ Erreur interne getSunoTimestamps:", error);
return {
success: false,
taskId: data?.taskId,
musicIndex: data?.musicIndex,
error: {
message: error.message,
type: "INTERNAL_ERROR",
},
};
}
});
/**
* Cloud Function pour recevoir les callbacks de l'API Suno
* Cette fonction est appelée par l'API Suno lorsque la génération
@@ -340,7 +466,9 @@ exports.sunoCallback = onRequest({ methods: ["POST"] }, async (req, res) => {
const body = req.body || {};
const code = body.code ?? body.statusCode ?? null;
const callbackType = (body?.data?.callbackType || "").toString().toLowerCase();
const callbackType = (body?.data?.callbackType || "")
.toString()
.toLowerCase();
const status = (body.status || body.state || callbackType)
.toString()
.toLowerCase();
@@ -402,10 +530,7 @@ exports.sunoCallback = onRequest({ methods: ["POST"] }, async (req, res) => {
const audioUrls = tracks
.map(
(t) =>
t.audio_url ||
t.audioUrl ||
t.stream_audio_url ||
t.streamAudioUrl,
t.audio_url || t.audioUrl || t.stream_audio_url || t.streamAudioUrl
)
.filter(Boolean)
.slice(0, 2);
@@ -420,7 +545,7 @@ exports.sunoCallback = onRequest({ methods: ["POST"] }, async (req, res) => {
has_audio_url: !!t.audio_url,
has_stream_audio_url: !!t.stream_audio_url,
})),
},
}
);
}
@@ -445,14 +570,14 @@ exports.sunoCallback = onRequest({ methods: ["POST"] }, async (req, res) => {
},
});
const downloadUrl = `https://firebasestorage.googleapis.com/v0/b/${bucket.name}/o/${encodeURIComponent(
path,
path
)}?alt=media&token=${token}`;
console.log("✅ [SunoCallback] Sauvegardé:", path, "URL:", downloadUrl);
return { path, url: downloadUrl };
} catch (e) {
console.error(
`❌ [SunoCallback] Échec save piste ${index + 1}:`,
e.message,
e.message
);
return null;
}
@@ -472,7 +597,7 @@ exports.sunoCallback = onRequest({ methods: ["POST"] }, async (req, res) => {
musicUrls,
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
},
{ merge: true },
{ merge: true }
);
console.log("🏷️ [SunoCallback] Projet marqué GENERATED", {
projectId,