clear last tickets
This commit is contained in:
+117
-210
@@ -10,6 +10,8 @@ const axios = require("axios");
|
||||
const { FieldValue } = require("firebase-admin/firestore");
|
||||
const { refList } = require("../index");
|
||||
|
||||
// --- CONSTANTES DE STRUCTURE ---
|
||||
// On garde ces mappings car ils sont utiles pour normaliser l'input utilisateur
|
||||
const STRUCTURE_PROMPT_LABELS = {
|
||||
couplet: "couplet",
|
||||
refrain: "refrain",
|
||||
@@ -60,6 +62,8 @@ const STRUCTURE_ALIASES = {
|
||||
soft_transition: "transition_douce",
|
||||
};
|
||||
|
||||
// --- UTILITAIRES ---
|
||||
|
||||
const normalizeStructureValue = (value) => {
|
||||
const raw = String(value || "")
|
||||
.trim()
|
||||
@@ -68,47 +72,16 @@ const normalizeStructureValue = (value) => {
|
||||
if (STRUCTURE_PROMPT_LABELS[raw]) return raw;
|
||||
if (STRUCTURE_ALIASES[raw]) return STRUCTURE_ALIASES[raw];
|
||||
const sanitized = raw.replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
||||
if (STRUCTURE_PROMPT_LABELS[sanitized]) return sanitized;
|
||||
if (STRUCTURE_ALIASES[sanitized]) return STRUCTURE_ALIASES[sanitized];
|
||||
return sanitized;
|
||||
return STRUCTURE_PROMPT_LABELS[sanitized]
|
||||
? sanitized
|
||||
: STRUCTURE_ALIASES[sanitized] || sanitized;
|
||||
};
|
||||
|
||||
// Nettoyage simplifié : on laisse l'IA gérer la logique musicale plutôt que de supprimer brutalement.
|
||||
// On s'assure juste que les clés sont propres.
|
||||
const sanitizeStructureEntries = (structure = []) => {
|
||||
if (!Array.isArray(structure)) return [];
|
||||
const output = [];
|
||||
structure.forEach((entry) => {
|
||||
const normalized = normalizeStructureValue(entry);
|
||||
if (!normalized) return;
|
||||
if (normalized === "short_intro" || normalized === "long_intro") {
|
||||
const existingIndex = output.findIndex(
|
||||
(item) => item === "short_intro" || item === "long_intro",
|
||||
);
|
||||
if (existingIndex !== -1) output.splice(existingIndex, 1);
|
||||
output.push(normalized);
|
||||
return;
|
||||
}
|
||||
if (normalized === "pre_refrain_instrumental") {
|
||||
output.push("pre_refrain_instrumental");
|
||||
return;
|
||||
}
|
||||
if (
|
||||
normalized === "final_apogee" ||
|
||||
normalized === "arret_net" ||
|
||||
normalized === "fade_out" ||
|
||||
normalized === "transition_douce"
|
||||
) {
|
||||
const outroIndex = output.findIndex((item) =>
|
||||
["final_apogee", "arret_net", "fade_out", "transition_douce"].includes(
|
||||
item,
|
||||
),
|
||||
);
|
||||
if (outroIndex !== -1) {
|
||||
output.splice(outroIndex, 1);
|
||||
}
|
||||
}
|
||||
output.push(normalized);
|
||||
});
|
||||
return output;
|
||||
return structure.map(normalizeStructureValue).filter(Boolean);
|
||||
};
|
||||
|
||||
const mapStructureToPrompt = (structure = []) =>
|
||||
@@ -116,16 +89,7 @@ const mapStructureToPrompt = (structure = []) =>
|
||||
(entry) => STRUCTURE_PROMPT_LABELS[entry] || entry,
|
||||
);
|
||||
|
||||
const TOXIC_KEYWORDS = [
|
||||
/\bnazis?\b/i,
|
||||
/\bnazisme\b/i,
|
||||
/\bnéo[- ]?nazis?\b/i,
|
||||
/\banti[- ]?s[eé]mites?\b/i,
|
||||
/\bantis[eé]mitisme\b/i,
|
||||
/\bracis(tes?|me)\b/i,
|
||||
/\bwhite\s+supremac(?:y|istes?|ism)\b/i,
|
||||
/\bku\s*klux\s*klan\b/i,
|
||||
];
|
||||
// --- MODERATION ---
|
||||
|
||||
const buildModerationBrief = ({
|
||||
objective,
|
||||
@@ -144,34 +108,13 @@ const buildModerationBrief = ({
|
||||
`<AUDIENCE>${audience || ""}</AUDIENCE>`,
|
||||
`<RIMES>${Array.isArray(rhymes) ? rhymes.join(", ") : rhymes || ""}</RIMES>`,
|
||||
];
|
||||
|
||||
const promptStructure = mapStructureToPrompt(structure);
|
||||
if (promptStructure.length) {
|
||||
if (promptStructure.length)
|
||||
sections.push(`<STRUCTURE>${promptStructure.join(" | ")}</STRUCTURE>`);
|
||||
}
|
||||
|
||||
return `<BRIEF_UTILISATEUR>\n${sections.join("\n")}\n</BRIEF_UTILISATEUR>`;
|
||||
};
|
||||
|
||||
const moderationIndicatesBlock = (moderation = {}, fallbackText = "") => {
|
||||
if (!moderation || typeof moderation !== "object") return false;
|
||||
|
||||
if (moderation.blocked === true) return true;
|
||||
|
||||
const excerpts = Array.isArray(moderation.excerpts)
|
||||
? moderation.excerpts
|
||||
: [];
|
||||
const reasons = Array.isArray(moderation.reasons) ? moderation.reasons : [];
|
||||
|
||||
const keywordDetected = (value) =>
|
||||
typeof value === "string" && TOXIC_KEYWORDS.some((rx) => rx.test(value));
|
||||
|
||||
if (excerpts.some((item) => keywordDetected(item?.quote))) return true;
|
||||
if (reasons.some((item) => keywordDetected(item))) return true;
|
||||
if (keywordDetected(fallbackText)) return true;
|
||||
|
||||
return false;
|
||||
};
|
||||
// --- GENERATION DE PAROLES (MAIN) ---
|
||||
|
||||
exports.generateLyrics = onCall({}, async ({ auth = {}, data = {} }) => {
|
||||
try {
|
||||
@@ -185,6 +128,7 @@ exports.generateLyrics = onCall({}, async ({ auth = {}, data = {} }) => {
|
||||
rhymes = "",
|
||||
} = data;
|
||||
|
||||
// 1. Préparation Structure
|
||||
const sanitizedStructure = sanitizeStructureEntries(rawStructure);
|
||||
const promptStructure = sanitizedStructure.length
|
||||
? sanitizedStructure.map(
|
||||
@@ -192,6 +136,8 @@ exports.generateLyrics = onCall({}, async ({ auth = {}, data = {} }) => {
|
||||
)
|
||||
: [];
|
||||
|
||||
// 2. Modération "Pro" (Via Gemini 1.5 Pro)
|
||||
// On supprime les regex manuelles obsolètes.
|
||||
const moderationPayload = {
|
||||
objective,
|
||||
context,
|
||||
@@ -201,59 +147,62 @@ exports.generateLyrics = onCall({}, async ({ auth = {}, data = {} }) => {
|
||||
structure: promptStructure,
|
||||
rhymes,
|
||||
};
|
||||
|
||||
const moderationInput = buildModerationBrief(moderationPayload);
|
||||
|
||||
try {
|
||||
const moderation = await analyseLyrics({
|
||||
title: "Brief utilisateur",
|
||||
title: "Brief utilisateur (Pré-génération)",
|
||||
lyrics: moderationInput,
|
||||
});
|
||||
|
||||
if (moderationIndicatesBlock(moderation, moderationInput)) {
|
||||
console.warn("generateLyrics blocked by moderation", {
|
||||
reasons: moderation?.reasons,
|
||||
excerpts: moderation?.excerpts,
|
||||
// Si Gemini dit "Blocked", on bloque. C'est la seule autorité.
|
||||
if (moderation?.blocked === true) {
|
||||
console.warn("⛔ generateLyrics blocked by Gemini Pro", {
|
||||
reasons: moderation.reasons,
|
||||
});
|
||||
|
||||
const summary = Array.isArray(moderation?.reasons)
|
||||
const summary = Array.isArray(moderation.reasons)
|
||||
? moderation.reasons.slice(0, 3).join(", ")
|
||||
: "contenu sensible";
|
||||
|
||||
: "Contenu non conforme";
|
||||
throw new HttpsError(
|
||||
"invalid-argument",
|
||||
`Certaines informations saisies contiennent des termes interdits (${summary}). Merci de reformuler pour poursuivre la création des paroles.`,
|
||||
{ moderation },
|
||||
`Demande refusée par la modération : ${summary}.`,
|
||||
);
|
||||
}
|
||||
} catch (moderationError) {
|
||||
if (moderationError instanceof HttpsError) {
|
||||
throw moderationError;
|
||||
}
|
||||
|
||||
console.error("generateLyrics moderation failure", moderationError);
|
||||
if (moderationError instanceof HttpsError) throw moderationError;
|
||||
console.error("⚠️ Moderation check error (fail open)", moderationError);
|
||||
// On continue si l'appel modération fail (fail open) ou on throw (fail closed) selon ta politique.
|
||||
// Ici je fail closed par sécurité pour une app publique.
|
||||
throw new HttpsError(
|
||||
"internal",
|
||||
"Impossible de vérifier le brief pour le moment. Merci de réessayer dans quelques instants.",
|
||||
"Vérification de sécurité indisponible.",
|
||||
);
|
||||
}
|
||||
|
||||
console.log("Function generate lyrics start with data", data);
|
||||
console.log("🎵 generateLyrics: Start generation", { style, emotion });
|
||||
|
||||
// 3. Le Prompt "Hit Maker"
|
||||
const system = `
|
||||
Tu es un parolier expert en chanson française moderne et populaire.
|
||||
Analyse attentivement les informations placées entre balises XML-like.
|
||||
Génère un titre accrocheur et mémorable en plus des paroles, tout en respectant strictement la structure demandée.
|
||||
Retourne uniquement un JSON conforme au schéma fourni, sans ajouter d'autres textes.
|
||||
Tu es le meilleur parolier musical actuel, expert en "Songwriting" pour les IA génératives audio (Suno/Udio).
|
||||
Ta spécialité est la **Prosodie** (le rythme naturel des mots) et l'impact émotionnel.
|
||||
|
||||
TES RÈGLES D'OR :
|
||||
1. **MÉTRIQUE & RYTHME** : Tes vers doivent être "chantables". Compte les syllabes pour qu'elles collent au style musical demandé. Évite les phrases trop longues ou imprononçables rapidement.
|
||||
2. **RIMES RICHES** : Évite les rimes faciles (amour/toujours). Cherche des assonances et des rimes internes.
|
||||
3. **SHOW, DON'T TELL** : N'écris pas "Je suis triste", écris "La pluie brouille mes carreaux". Utilise des détails sensoriels.
|
||||
4. **STRUCTURE STRICTE** : Respecte exactement l'ordre des sections demandées.
|
||||
- **Couplet** : Raconte l'histoire, pose le décor.
|
||||
- **Refrain** : Le "Hook". Doit être simple, répétitif, accrocheur et résumer l'émotion principale.
|
||||
- **Pont** : Change de rythme ou de perspective, crée une tension avant le dernier refrain.
|
||||
5. **VOCABULAIRE** : Adapte le niveau de langue au style (Argot pour le Rap, Soutenu pour la Chanson Française, Simple pour la Pop).
|
||||
|
||||
Retourne UNIQUEMENT un JSON valide.
|
||||
`.trim();
|
||||
|
||||
const structureTags = (
|
||||
Array.isArray(promptStructure) ? promptStructure : []
|
||||
)
|
||||
.map((part, index) => {
|
||||
const content = part || "";
|
||||
return ` <SECTION ordre="${index + 1}">${content}</SECTION>`;
|
||||
})
|
||||
.map((part, index) => ` <SECTION ordre="${index + 1}">${part}</SECTION>`)
|
||||
.join("\n");
|
||||
|
||||
const fallbackStructureTags = [
|
||||
@@ -261,40 +210,29 @@ Retourne uniquement un JSON conforme au schéma fourni, sans ajouter d'autres te
|
||||
' <SECTION ordre="2">refrain</SECTION>',
|
||||
].join("\n");
|
||||
|
||||
// Construction du prompt utilisateur avec des balises explicites
|
||||
const prompt = `
|
||||
<BRIEF>
|
||||
<OBJECTIF>${objective || ""}</OBJECTIF>
|
||||
<CONTEXTE>${context || ""}</CONTEXTE>
|
||||
<EMOTION>${emotion || ""}</EMOTION>
|
||||
<STYLE_MUSICAL>${style || ""}</STYLE_MUSICAL>
|
||||
<AUDIENCE>${audience || ""}</AUDIENCE>
|
||||
<SCHEMA_DE_RIMES>${rhymes || ""}</SCHEMA_DE_RIMES>
|
||||
</BRIEF>
|
||||
<BRIEF_CREATIF>
|
||||
<OBJECTIF>${objective || "Créer une chanson mémorable"}</OBJECTIF>
|
||||
<CONTEXTE>${context || "Libre interprétation"}</CONTEXTE>
|
||||
<EMOTION_DOMINANTE>${emotion || "Intense"}</EMOTION_DOMINANTE>
|
||||
<STYLE_MUSICAL>${style || "Pop Moderne"}</STYLE_MUSICAL>
|
||||
<CIBLE>${audience || "Tout public"}</CIBLE>
|
||||
<TYPE_DE_RIMES>${rhymes || "Rimes croisées et riches"}</TYPE_DE_RIMES>
|
||||
</BRIEF_CREATIF>
|
||||
|
||||
<STRUCTURE_DEMANDEE>
|
||||
<STRUCTURE_IMPOSEE>
|
||||
${structureTags || fallbackStructureTags}
|
||||
</STRUCTURE_DEMANDEE>
|
||||
</STRUCTURE_IMPOSEE>
|
||||
|
||||
<CONTRAINTES>
|
||||
<TITRE>
|
||||
Génère un titre court, mémorable et aligné avec l'esprit de la musique.
|
||||
</TITRE>
|
||||
<PAROLES>
|
||||
Pour chaque section demandée, rédige entre 4 et 16 lignes expressives.
|
||||
Respecte strictement l'ordre et le type de chaque section (couplet, refrain, etc.).
|
||||
</PAROLES>
|
||||
<STYLE>
|
||||
Adapte le vocabulaire et le ton aux informations fournies dans le brief et la structure.
|
||||
</STYLE>
|
||||
<FORMAT>
|
||||
Retourne uniquement un objet JSON valide selon le schéma.
|
||||
Utilise les retours à la ligne (\n) pour séparer les phrases afin de conserver le découpage.
|
||||
</FORMAT>
|
||||
</CONTRAINTES>
|
||||
<CONSIGNES_GENERATION>
|
||||
1. Génère un **Titre** percutant (moins de 6 mots).
|
||||
2. Pour chaque section <SECTION>, écris le contenu adapté :
|
||||
- Si c'est "Instrumental" ou "Solo" : laisse le champ 'lyrics' vide ou mets une brève indication d'ambiance entre parenthèses ex: "(Solo de guitare déchirant)".
|
||||
- Si c'est "Couplet/Refrain" : Écris 4 à 12 vers.
|
||||
3. **IMPORTANT** : Le style est "${style}". Assure-toi que le vocabulaire et le rythme collent parfaitement à ce genre.
|
||||
</CONSIGNES_GENERATION>
|
||||
`.trim();
|
||||
|
||||
// Définition du schéma zod/genkit pour la validation du retour
|
||||
const lyricsSchema = z.object({
|
||||
title: z.string().describe("Titre de la chanson, court et accrocheur"),
|
||||
lyrics: z
|
||||
@@ -303,48 +241,51 @@ ${structureTags || fallbackStructureTags}
|
||||
type: z
|
||||
.string()
|
||||
.describe(
|
||||
"Type de section : respecter exactement le nom de la section demandé dans la structure",
|
||||
"Type de section (copier exactement la demande structure)",
|
||||
),
|
||||
lyrics: z
|
||||
.string()
|
||||
.describe(
|
||||
"Paroles de la section, chaque ligne séparée par un retour à la ligne",
|
||||
"Les paroles. Pour les sections instrumentales, laisser vide ou décrire l'ambiance.",
|
||||
),
|
||||
}),
|
||||
)
|
||||
.describe(
|
||||
"Paroles de la chanson sous forme de tableau de sections structurées.",
|
||||
),
|
||||
.describe("La structure complète de la chanson"),
|
||||
lyricsDescription: z
|
||||
.string()
|
||||
.describe("Courte déscription qui résume les paroles de la musique"),
|
||||
success: z.boolean().describe("Indique si la génération a réussi"),
|
||||
.describe(
|
||||
"Un pitch de 2 phrases décrivant l'ambiance et le thème de la chanson pour Suno.",
|
||||
),
|
||||
success: z.boolean(),
|
||||
});
|
||||
|
||||
// Appel Gemini avec le nouveau modèle Pro configuré dans helpers/gemini
|
||||
return await generateAI({
|
||||
system,
|
||||
prompt,
|
||||
schema: lyricsSchema,
|
||||
});
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
console.error("❌ generateLyrics Error:", e);
|
||||
// Remontée d'erreur propre
|
||||
if (e instanceof HttpsError) throw e;
|
||||
throw new HttpsError(
|
||||
"internal",
|
||||
"Erreur lors de la génération des paroles.",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// --- ANALYSE TOXICITÉ (EXISTANTE, NETTOYÉE) ---
|
||||
// Cette fonction reste utile pour l'analyse POST-création ou pour l'interface UI
|
||||
|
||||
const severityScore = (severity = "") => {
|
||||
const normalized = String(severity || "").toLowerCase();
|
||||
switch (normalized) {
|
||||
case "critical":
|
||||
return 4;
|
||||
case "high":
|
||||
return 3;
|
||||
case "medium":
|
||||
return 2;
|
||||
case "low":
|
||||
return 1;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
if (normalized === "critical") return 4;
|
||||
if (normalized === "high") return 3;
|
||||
if (normalized === "medium") return 2;
|
||||
if (normalized === "low") return 1;
|
||||
return 0;
|
||||
};
|
||||
|
||||
const softenModerationDecision = (rawResult = {}) => {
|
||||
@@ -361,6 +302,7 @@ const softenModerationDecision = (rawResult = {}) => {
|
||||
? Math.min(Math.max(result.score, 0), 1)
|
||||
: 0;
|
||||
|
||||
// Détection de contexte narratif pour être plus indulgent
|
||||
const narrativeHint = reasons.some((reason) =>
|
||||
/narrati|story|persona|fiction|metaphor|metaphore|récit|roleplay|contexte/i.test(
|
||||
reason || "",
|
||||
@@ -369,9 +311,11 @@ const softenModerationDecision = (rawResult = {}) => {
|
||||
|
||||
const adjustments = [];
|
||||
|
||||
// Logique d'adoucissement : Si c'est "High" severity mais narratif, on peut parfois débloquer (selon ta politique).
|
||||
// Ici on reste prudent sur le block, mais on adoucit le flag.
|
||||
if (result.blocked) {
|
||||
const shouldUnblockLowSeverity = highestSeverity <= 1 && score < 0.65;
|
||||
if (shouldUnblockLowSeverity) {
|
||||
// Débloque uniquement les erreurs manifestes (score bas mais blocked true par erreur)
|
||||
if (highestSeverity <= 1 && score < 0.65) {
|
||||
result.blocked = false;
|
||||
adjustments.push("auto-unblock-low-severity");
|
||||
}
|
||||
@@ -380,6 +324,7 @@ const softenModerationDecision = (rawResult = {}) => {
|
||||
if (!result.blocked && result.flagged) {
|
||||
const lowSignal = highestSeverity <= 1 && score < 0.4;
|
||||
const contextual = narrativeHint && highestSeverity <= 2 && score < 0.55;
|
||||
|
||||
if (lowSignal) {
|
||||
result.flagged = false;
|
||||
adjustments.push("drop-flag-low-signal");
|
||||
@@ -392,18 +337,12 @@ const softenModerationDecision = (rawResult = {}) => {
|
||||
return {
|
||||
...result,
|
||||
moderationAdjustments: adjustments,
|
||||
moderationCalibration: {
|
||||
highestSeverity,
|
||||
score,
|
||||
},
|
||||
moderationCalibration: { highestSeverity, score },
|
||||
};
|
||||
};
|
||||
|
||||
// Vérifie la toxicité des paroles via Gemini et retourne un résultat structuré
|
||||
// Nom Cloud Function: lyrics-analyseLyricsToxicity
|
||||
exports.analyseLyricsToxicity = onCall({}, async ({ data = {} }) => {
|
||||
try {
|
||||
// Validation d'entrée minimale
|
||||
const requestSchema = z.object({
|
||||
title: z.string().optional(),
|
||||
lyrics: z
|
||||
@@ -420,7 +359,7 @@ exports.analyseLyricsToxicity = onCall({}, async ({ data = {} }) => {
|
||||
}),
|
||||
),
|
||||
])
|
||||
.describe("Paroles à analyser (formats supportés)"),
|
||||
.describe("Paroles à analyser"),
|
||||
});
|
||||
|
||||
const parsed = requestSchema.parse(data || {});
|
||||
@@ -430,8 +369,7 @@ exports.analyseLyricsToxicity = onCall({}, async ({ data = {} }) => {
|
||||
});
|
||||
const result = softenModerationDecision(aiResult);
|
||||
|
||||
// Prépare un message lisible pour le front
|
||||
const highCats = Object.entries(result.categories || {})
|
||||
const highCats = Object.entries(result.categories || {}) // Note: categories n'est pas dans le schema Zod actuel de Gemini, à vérifier si besoin
|
||||
.filter(([, v]) => (typeof v === "number" ? v : 0) >= 0.5)
|
||||
.map(([k]) => k)
|
||||
.slice(0, 5);
|
||||
@@ -441,60 +379,41 @@ exports.analyseLyricsToxicity = onCall({}, async ({ data = {} }) => {
|
||||
|
||||
if (result.blocked) {
|
||||
errorCode = "TOXIC_CONTENT_BLOCKED";
|
||||
const list = highCats.length ? ` (${highCats.join(", ")})` : "";
|
||||
message = `Publication bloquée: contenu interdit détecté${list}.`;
|
||||
message = `Contenu bloqué par sécurité.`;
|
||||
} else if (result.flagged) {
|
||||
errorCode = "TOXIC_CONTENT_FLAGGED";
|
||||
const list = highCats.length ? ` (${highCats.join(", ")})` : "";
|
||||
message = `Attention: contenu potentiellement sensible détecté${list}.`;
|
||||
message = `Attention: contenu sensible détecté.`;
|
||||
}
|
||||
|
||||
return {
|
||||
success: !result.blocked,
|
||||
errorCode,
|
||||
message,
|
||||
result,
|
||||
};
|
||||
return { success: !result.blocked, errorCode, message, result };
|
||||
} catch (err) {
|
||||
console.error("analyseLyricsToxicity failed", err?.message || err);
|
||||
return {
|
||||
success: false,
|
||||
errorCode: "ANALYSE_FAILED",
|
||||
message: "Erreur lors de l'analyse de la toxicité. Réessayez plus tard.",
|
||||
message: "Erreur analyse toxicité.",
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Récupère les timestamps (aligned words) pour une génération Suno
|
||||
* Attend: { projectId: string }
|
||||
*/
|
||||
// --- SUNO TIMESTAMPS (EXISTANT) ---
|
||||
|
||||
async function getSunoTimestamps(projectId) {
|
||||
try {
|
||||
const { sunoTaskId, songIndex } = (
|
||||
await refList.projects.doc(projectId).get()
|
||||
).data();
|
||||
if (!sunoTaskId) {
|
||||
throw new Error("TaskId manquant");
|
||||
}
|
||||
if (!projectId || typeof projectId !== "string")
|
||||
throw new Error("projectId invalide");
|
||||
|
||||
if (songIndex < 0) {
|
||||
const docSnap = await refList.projects.doc(projectId).get();
|
||||
if (!docSnap.exists) throw new Error("Projet introuvable");
|
||||
|
||||
const { sunoTaskId, songIndex } = docSnap.data();
|
||||
if (!sunoTaskId) throw new Error("TaskId manquant");
|
||||
if (songIndex === undefined || songIndex < 0)
|
||||
throw new Error("musicIndex invalide");
|
||||
}
|
||||
|
||||
if (!projectId || typeof projectId !== "string" || !projectId.trim()) {
|
||||
throw new Error("projectId manquant ou invalide");
|
||||
}
|
||||
console.log("🔎 getSunoTimestamps", { sunoTaskId, songIndex });
|
||||
|
||||
console.log("🔎 Récupération des timestamps Suno", {
|
||||
sunoTaskId,
|
||||
songIndex,
|
||||
});
|
||||
|
||||
let response;
|
||||
let parsed;
|
||||
|
||||
response = await axios.post(
|
||||
const response = await axios.post(
|
||||
`${SUNO_API_BASE}${SUNO_TIMESTAMPED_LYRICS_PATH}`,
|
||||
{ taskId: sunoTaskId, musicIndex: songIndex },
|
||||
{
|
||||
@@ -505,35 +424,23 @@ async function getSunoTimestamps(projectId) {
|
||||
timeout: 30000,
|
||||
},
|
||||
);
|
||||
parsed = response.data;
|
||||
console.log("📊 Réponse timestamps Suno API:", parsed?.code, parsed?.msg);
|
||||
|
||||
const dataToReturn = parsed?.data || parsed || {};
|
||||
const dataToReturn = response.data?.data || response.data || {};
|
||||
|
||||
await refList.projects.doc(projectId).set(
|
||||
{
|
||||
musicTimestamps: {
|
||||
[songIndex]: dataToReturn,
|
||||
},
|
||||
musicTimestamps: { [songIndex]: dataToReturn },
|
||||
updatedAt: FieldValue.serverTimestamp(),
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
console.log("💾 Timestamps sauvegardés dans Firestore", {
|
||||
projectId,
|
||||
songIndex,
|
||||
});
|
||||
return {
|
||||
success: true,
|
||||
};
|
||||
|
||||
return { success: true };
|
||||
} catch (error) {
|
||||
console.error("❌ Erreur interne getSunoTimestamps:", error);
|
||||
console.error("❌ getSunoTimestamps Error:", error.message);
|
||||
return {
|
||||
success: false,
|
||||
error: {
|
||||
message: error.message,
|
||||
type: "INTERNAL_ERROR",
|
||||
},
|
||||
error: { message: error.message, type: "INTERNAL_ERROR" },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user