filter on lyrics for bloking unwanted content, tickets fix
This commit is contained in:
+118
-14
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user