filter on lyrics for bloking unwanted content, tickets fix

This commit is contained in:
Thomas Demirdjian
2025-09-08 15:48:29 +02:00
parent 987388c501
commit a3dc4f89b7
22 changed files with 1386 additions and 126 deletions
+118 -14
View File
@@ -32,6 +32,97 @@ function clampLen(str = "", max) {
return str.length <= max ? str : str.slice(0, max);
}
/**
* Déduit des balises vocales explicites reconnues par les modèles (EN)
* à partir de la description utilisateur (FR)
*/
function extractVoiceStrings(voice) {
if (!voice) return [];
if (typeof voice === "string") return [voice];
if (Array.isArray(voice)) {
return voice
.map((v) => (typeof v === "string" ? v : v?.value || v?.text || ""))
.filter(Boolean);
}
if (typeof voice === "object") {
return Object.values(voice).filter((v) => typeof v === "string" && v.trim());
}
return [];
}
function voiceTagsFromUserChoice(voiceInput = "") {
const all = extractVoiceStrings(voiceInput);
const tags = [];
const addFromText = (txt) => {
const v = String(txt || "").toLowerCase();
// Gender / ensemble
if (/féminin|feminin|féminine|feminine/.test(v)) tags.push("Female lead vocal");
if (/masculin|masculine/.test(v)) tags.push("Male lead vocal");
if (/deux voix|duo|deux chanteurs/.test(v)) tags.push("Duet (male and female voices)");
if (/choeu?r|gospel/.test(v)) tags.push("Gospel choir vocals");
// Delivery/techniques
if (/rap\b/.test(v)) tags.push("Rap vocal");
if (/slam|parl[ée]e|chant parl[ée]/.test(v)) tags.push("Spoken word / slam");
if (/raconte|narrat/.test(v)) tags.push("Narration / spoken narrator");
if (/cri|scream|rugueu|growl/.test(v)) tags.push("Screamed vocals");
if (/sprech/.test(v)) tags.push("Sprechgesang (sung-spoken) style");
// Tone/texture
if (/séduis|sensuel/.test(v)) tags.push("Seductive, sensual tone");
if (/profonde|r[ée]sonnan/.test(v)) tags.push("Deep, resonant voice");
if (/l[ée]g[èe]re|a[ée]rienne/.test(v)) tags.push("Light, airy voice");
if (/relaxante|sophistiqu[ée]/.test(v)) tags.push("Smooth, lounge vocal");
if (/synth[ée]tique|robot/.test(v)) tags.push("Synthetic/processed vocal");
if (/d[ée]coup|chopp/.test(v)) tags.push("Chopped vocal samples");
// Choir color
if (/c[ée]leste|divin|angel/i.test(v)) tags.push("Ethereal, angelic choir");
};
if (all.length === 0) addFromText(voiceInput);
else all.forEach(addFromText);
return Array.from(new Set(tags));
}
/**
* Détecte un genre vocal simple (m/f) pour Suno à partir du texte utilisateur.
*/
function detectVocalGender(voiceInput = "") {
// If array of objects with category, prefer 'base'
if (Array.isArray(voiceInput)) {
const baseItem = voiceInput.find((v) => v && (v.category === "base" || v?.category?.toLowerCase() === "base"));
const text = baseItem ? baseItem.value || baseItem.text || baseItem : null;
if (text) return detectVocalGender(text);
// Fallback: scan all
for (const it of voiceInput) {
const g = detectVocalGender(it?.value || it?.text || it);
if (g) return g;
}
return undefined;
}
if (typeof voiceInput === "object" && voiceInput) {
// object keyed by category
const base = voiceInput.base || voiceInput["base"];
if (base) return detectVocalGender(base);
const vals = Object.values(voiceInput).filter(Boolean);
for (const v of vals) {
const g = detectVocalGender(v);
if (g) return g;
}
return undefined;
}
const v = String(voiceInput || "").toLowerCase();
if (!v) return undefined;
if (/(féminin|feminin|féminine|feminine|voix\s*f[ée]min)/.test(v)) return "f";
if (/(masculin|masculine|voix\s*mascul)/.test(v)) return "m";
if (/(femme)/.test(v)) return "f";
if (/(homme)/.test(v)) return "m";
return undefined;
}
/**
* Construit le style musical à partir des paramètres
* @param {Object} params - Les paramètres de style
@@ -42,12 +133,17 @@ function buildStyle({ genres = [], voice = "", instruments = [], tempo = "" }) {
const flatInstruments = (instruments || [])
.map((i) => ("" + i).trim())
.filter(Boolean);
const voiceTags = voiceTagsFromUserChoice(voice);
const voiceStrings = extractVoiceStrings(voice);
const voiceUserText = voiceStrings.join(" ; ");
const styleParts = [
flatGenres.length ? `Genres: ${flatGenres.join(", ")}` : "",
tempo ? `Tempo: ${tempo}` : "",
flatInstruments.length ? `Instruments: ${flatInstruments.join(", ")}` : "",
voice ? `Vocal: ${voice}` : "",
// Inject tags AND the raw user string to fully convey preference
voiceTags.length ? `Vocal: ${voiceTags.join(", ")}` : "",
voiceUserText ? `Vocal (user): ${voiceUserText}` : "",
"Mix: propre, punchy, large stéréo, radio-ready",
].filter(Boolean);
@@ -132,8 +228,11 @@ function buildGuides({
styleElements.push(`Instruments principaux: ${instruments.join(", ")}`);
}
if (voice) {
styleElements.push(`Style vocal: ${voice}`);
const voiceStrings = extractVoiceStrings(voice);
if (voiceStrings.length) {
const tags = voiceTagsFromUserChoice(voice);
if (tags.length) styleElements.push(`Vocal direction: ${tags.join(", ")}`);
styleElements.push(`User vocal request: ${voiceStrings.join(" ; ")}`);
}
return {
@@ -191,6 +290,10 @@ exports.generateMusic = onCall(async ({ data = {} }) => {
callBackUrl: SUNO_CALLBACK_URL || "",
};
// Appliquer la préférence de genre vocal si détectée (doc: vocalGender: "m" | "f")
const vGender = detectVocalGender(voice);
if (vGender) payload.vocalGender = vGender;
console.log("PAYLOAD", payload);
const response = await axios.post(
@@ -201,7 +304,7 @@ exports.generateMusic = onCall(async ({ data = {} }) => {
"Content-Type": "application/json",
Authorization: `Bearer ${SUNO_API_KEY}`,
},
}
},
);
const parsed = response.data;
@@ -214,13 +317,14 @@ exports.generateMusic = onCall(async ({ data = {} }) => {
title: safeTitle,
style: enhancedStyle,
prompt: taggedLyrics,
vocalGender: vGender || null,
},
response: parsed || {},
};
} 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}`,
);
}
});
@@ -249,7 +353,7 @@ exports.getSunoStatus = onCall(async ({ data = {} }) => {
Authorization: `Bearer ${SUNO_API_KEY}`,
},
timeout: 30000, // 30 secondes de timeout
}
},
);
parsed = response.data;
@@ -365,7 +469,7 @@ exports.getSunoTimestamps = onCall(async ({ data = {} }) => {
Authorization: `Bearer ${SUNO_API_KEY}`,
},
timeout: 30000,
}
},
);
parsed = response.data;
console.log("📊 Réponse timestamps Suno API:", parsed?.code, parsed?.msg);
@@ -417,7 +521,7 @@ exports.getSunoTimestamps = onCall(async ({ data = {} }) => {
},
updatedAt: admin.firestore.FieldValue.serverTimestamp(),
},
{ merge: true }
{ merge: true },
);
saved = true;
console.log("💾 Timestamps sauvegardés dans Firestore", {
@@ -427,7 +531,7 @@ exports.getSunoTimestamps = onCall(async ({ data = {} }) => {
} catch (e) {
console.error(
"❌ Échec sauvegarde timestamps Firestore:",
e?.message || e
e?.message || e,
);
}
@@ -535,7 +639,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);
@@ -550,7 +654,7 @@ exports.sunoCallback = onRequest({ methods: ["POST"] }, async (req, res) => {
has_audio_url: !!t.audio_url,
has_stream_audio_url: !!t.stream_audio_url,
})),
}
},
);
}
@@ -575,14 +679,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;
}
@@ -602,7 +706,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,