feat: fixes and formatter
This commit is contained in:
+191
-220
@@ -1,93 +1,86 @@
|
||||
const { onCall, HttpsError } = require("firebase-functions/v2/https");
|
||||
const { z } = require("genkit");
|
||||
const { generateAI, analyseLyrics } = require("../helpers/gemini");
|
||||
const {
|
||||
SUNO_API_BASE,
|
||||
SUNO_TIMESTAMPED_LYRICS_PATH,
|
||||
} = require("../config/suno");
|
||||
const { SUNO_API_KEY } = require("../config/keys");
|
||||
const axios = require("axios");
|
||||
const { FieldValue } = require("firebase-admin/firestore");
|
||||
const { refList } = require("../index");
|
||||
const { onCall, HttpsError } = require('firebase-functions/v2/https')
|
||||
const { z } = require('genkit')
|
||||
const { generateAI, analyseLyrics } = require('../helpers/gemini')
|
||||
const { SUNO_API_BASE, SUNO_TIMESTAMPED_LYRICS_PATH } = require('../config/suno')
|
||||
const { SUNO_API_KEY } = require('../config/keys')
|
||||
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",
|
||||
short_intro: "introduction instrumentale courte",
|
||||
long_intro: "introduction instrumentale longue",
|
||||
pre_refrain_instrumental: "pré-refrain instrumental",
|
||||
pont: "pont",
|
||||
solo_de_guitare: "solo de guitare",
|
||||
solo_de_guitare_electrique: "solo de guitare électrique",
|
||||
solo_de_batterie: "solo de batterie",
|
||||
solo_de_saxophone: "solo de saxophone",
|
||||
solo_de_violon: "solo de violon",
|
||||
break: "break",
|
||||
interlude: "interlude",
|
||||
interlude_melodique: "interlude mélodique",
|
||||
final_apogee: "final apogée",
|
||||
arret_net: "arrêt net",
|
||||
fade_out: "fade out",
|
||||
transition_douce: "transition douce vers le silence",
|
||||
};
|
||||
couplet: 'couplet',
|
||||
refrain: 'refrain',
|
||||
short_intro: 'introduction instrumentale courte',
|
||||
long_intro: 'introduction instrumentale longue',
|
||||
pre_refrain_instrumental: 'pré-refrain instrumental',
|
||||
pont: 'pont',
|
||||
solo_de_guitare: 'solo de guitare',
|
||||
solo_de_guitare_electrique: 'solo de guitare électrique',
|
||||
solo_de_batterie: 'solo de batterie',
|
||||
solo_de_saxophone: 'solo de saxophone',
|
||||
solo_de_violon: 'solo de violon',
|
||||
break: 'break',
|
||||
interlude: 'interlude',
|
||||
interlude_melodique: 'interlude mélodique',
|
||||
final_apogee: 'final apogée',
|
||||
arret_net: 'arrêt net',
|
||||
fade_out: 'fade out',
|
||||
transition_douce: 'transition douce vers le silence',
|
||||
}
|
||||
|
||||
const STRUCTURE_ALIASES = {
|
||||
"short intro": "short_intro",
|
||||
"introduction instrumentale courte": "short_intro",
|
||||
"intro instrumentale courte": "short_intro",
|
||||
"long intro": "long_intro",
|
||||
"introduction instrumentale longue": "long_intro",
|
||||
"intro instrumentale longue": "long_intro",
|
||||
"pré-refrain": "pre_refrain_instrumental",
|
||||
"pre-refrain": "pre_refrain_instrumental",
|
||||
pre_refrain: "pre_refrain_instrumental",
|
||||
"pre chorus": "pre_refrain_instrumental",
|
||||
"pre-chorus": "pre_refrain_instrumental",
|
||||
prechorus: "pre_refrain_instrumental",
|
||||
"pré-refrain instrumental": "pre_refrain_instrumental",
|
||||
"pre-refrain instrumental": "pre_refrain_instrumental",
|
||||
"instrumental pre-chorus": "pre_refrain_instrumental",
|
||||
"instrumental pre chorus": "pre_refrain_instrumental",
|
||||
bridge: "pont",
|
||||
guitar_solo: "solo_de_guitare",
|
||||
electric_guitar_solo: "solo_de_guitare_electrique",
|
||||
drum_solo: "solo_de_batterie",
|
||||
sax_solo: "solo_de_saxophone",
|
||||
violin_solo: "solo_de_violon",
|
||||
melodic_interlude: "interlude_melodique",
|
||||
grand_finale: "final_apogee",
|
||||
sudden_stop: "arret_net",
|
||||
soft_transition: "transition_douce",
|
||||
};
|
||||
'short intro': 'short_intro',
|
||||
'introduction instrumentale courte': 'short_intro',
|
||||
'intro instrumentale courte': 'short_intro',
|
||||
'long intro': 'long_intro',
|
||||
'introduction instrumentale longue': 'long_intro',
|
||||
'intro instrumentale longue': 'long_intro',
|
||||
'pré-refrain': 'pre_refrain_instrumental',
|
||||
'pre-refrain': 'pre_refrain_instrumental',
|
||||
pre_refrain: 'pre_refrain_instrumental',
|
||||
'pre chorus': 'pre_refrain_instrumental',
|
||||
'pre-chorus': 'pre_refrain_instrumental',
|
||||
prechorus: 'pre_refrain_instrumental',
|
||||
'pré-refrain instrumental': 'pre_refrain_instrumental',
|
||||
'pre-refrain instrumental': 'pre_refrain_instrumental',
|
||||
'instrumental pre-chorus': 'pre_refrain_instrumental',
|
||||
'instrumental pre chorus': 'pre_refrain_instrumental',
|
||||
bridge: 'pont',
|
||||
guitar_solo: 'solo_de_guitare',
|
||||
electric_guitar_solo: 'solo_de_guitare_electrique',
|
||||
drum_solo: 'solo_de_batterie',
|
||||
sax_solo: 'solo_de_saxophone',
|
||||
violin_solo: 'solo_de_violon',
|
||||
melodic_interlude: 'interlude_melodique',
|
||||
grand_finale: 'final_apogee',
|
||||
sudden_stop: 'arret_net',
|
||||
soft_transition: 'transition_douce',
|
||||
}
|
||||
|
||||
// --- UTILITAIRES ---
|
||||
|
||||
const normalizeStructureValue = (value) => {
|
||||
const raw = String(value || "")
|
||||
const raw = String(value || '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (!raw) return "";
|
||||
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, "");
|
||||
return STRUCTURE_PROMPT_LABELS[sanitized]
|
||||
? sanitized
|
||||
: STRUCTURE_ALIASES[sanitized] || sanitized;
|
||||
};
|
||||
.toLowerCase()
|
||||
if (!raw) return ''
|
||||
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, '')
|
||||
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 [];
|
||||
return structure.map(normalizeStructureValue).filter(Boolean);
|
||||
};
|
||||
if (!Array.isArray(structure)) return []
|
||||
return structure.map(normalizeStructureValue).filter(Boolean)
|
||||
}
|
||||
|
||||
const mapStructureToPrompt = (structure = []) =>
|
||||
sanitizeStructureEntries(structure).map(
|
||||
(entry) => STRUCTURE_PROMPT_LABELS[entry] || entry,
|
||||
);
|
||||
sanitizeStructureEntries(structure).map((entry) => STRUCTURE_PROMPT_LABELS[entry] || entry)
|
||||
|
||||
// --- MODERATION ---
|
||||
|
||||
@@ -101,40 +94,37 @@ const buildModerationBrief = ({
|
||||
rhymes,
|
||||
}) => {
|
||||
const sections = [
|
||||
`<OBJECTIF>${objective || ""}</OBJECTIF>`,
|
||||
`<CONTEXTE>${context || ""}</CONTEXTE>`,
|
||||
`<EMOTION>${emotion || ""}</EMOTION>`,
|
||||
`<STYLE>${style || ""}</STYLE>`,
|
||||
`<AUDIENCE>${audience || ""}</AUDIENCE>`,
|
||||
`<RIMES>${Array.isArray(rhymes) ? rhymes.join(", ") : rhymes || ""}</RIMES>`,
|
||||
];
|
||||
const promptStructure = mapStructureToPrompt(structure);
|
||||
if (promptStructure.length)
|
||||
sections.push(`<STRUCTURE>${promptStructure.join(" | ")}</STRUCTURE>`);
|
||||
return `<BRIEF_UTILISATEUR>\n${sections.join("\n")}\n</BRIEF_UTILISATEUR>`;
|
||||
};
|
||||
`<OBJECTIF>${objective || ''}</OBJECTIF>`,
|
||||
`<CONTEXTE>${context || ''}</CONTEXTE>`,
|
||||
`<EMOTION>${emotion || ''}</EMOTION>`,
|
||||
`<STYLE>${style || ''}</STYLE>`,
|
||||
`<AUDIENCE>${audience || ''}</AUDIENCE>`,
|
||||
`<RIMES>${Array.isArray(rhymes) ? rhymes.join(', ') : rhymes || ''}</RIMES>`,
|
||||
]
|
||||
const promptStructure = mapStructureToPrompt(structure)
|
||||
if (promptStructure.length) sections.push(`<STRUCTURE>${promptStructure.join(' | ')}</STRUCTURE>`)
|
||||
return `<BRIEF_UTILISATEUR>\n${sections.join('\n')}\n</BRIEF_UTILISATEUR>`
|
||||
}
|
||||
|
||||
// --- GENERATION DE PAROLES (MAIN) ---
|
||||
|
||||
exports.generateLyrics = onCall({}, async ({ auth = {}, data = {} }) => {
|
||||
try {
|
||||
const {
|
||||
objective = "",
|
||||
context = "",
|
||||
style = "",
|
||||
audience = "",
|
||||
emotion = "",
|
||||
structure: rawStructure = ["couplet", "refrain", "couplet", "refrain"],
|
||||
rhymes = "",
|
||||
} = data;
|
||||
objective = '',
|
||||
context = '',
|
||||
style = '',
|
||||
audience = '',
|
||||
emotion = '',
|
||||
structure: rawStructure = ['couplet', 'refrain', 'couplet', 'refrain'],
|
||||
rhymes = '',
|
||||
} = data
|
||||
|
||||
// 1. Préparation Structure
|
||||
const sanitizedStructure = sanitizeStructureEntries(rawStructure);
|
||||
const sanitizedStructure = sanitizeStructureEntries(rawStructure)
|
||||
const promptStructure = sanitizedStructure.length
|
||||
? sanitizedStructure.map(
|
||||
(entry) => STRUCTURE_PROMPT_LABELS[entry] || entry,
|
||||
)
|
||||
: [];
|
||||
? sanitizedStructure.map((entry) => STRUCTURE_PROMPT_LABELS[entry] || entry)
|
||||
: []
|
||||
|
||||
// 2. Modération "Pro" (Via Gemini 1.5 Pro)
|
||||
// On supprime les regex manuelles obsolètes.
|
||||
@@ -146,40 +136,34 @@ exports.generateLyrics = onCall({}, async ({ auth = {}, data = {} }) => {
|
||||
emotion,
|
||||
structure: promptStructure,
|
||||
rhymes,
|
||||
};
|
||||
const moderationInput = buildModerationBrief(moderationPayload);
|
||||
}
|
||||
const moderationInput = buildModerationBrief(moderationPayload)
|
||||
|
||||
try {
|
||||
const moderation = await analyseLyrics({
|
||||
title: "Brief utilisateur (Pré-génération)",
|
||||
title: 'Brief utilisateur (Pré-génération)',
|
||||
lyrics: moderationInput,
|
||||
});
|
||||
})
|
||||
|
||||
// Si Gemini dit "Blocked", on bloque. C'est la seule autorité.
|
||||
if (moderation?.blocked === true) {
|
||||
console.warn("⛔ generateLyrics blocked by Gemini Pro", {
|
||||
console.warn('⛔ generateLyrics blocked by Gemini Pro', {
|
||||
reasons: moderation.reasons,
|
||||
});
|
||||
})
|
||||
const summary = Array.isArray(moderation.reasons)
|
||||
? moderation.reasons.slice(0, 3).join(", ")
|
||||
: "Contenu non conforme";
|
||||
throw new HttpsError(
|
||||
"invalid-argument",
|
||||
`Demande refusée par la modération : ${summary}.`,
|
||||
);
|
||||
? moderation.reasons.slice(0, 3).join(', ')
|
||||
: 'Contenu non conforme'
|
||||
throw new HttpsError('invalid-argument', `Demande refusée par la modération : ${summary}.`)
|
||||
}
|
||||
} catch (moderationError) {
|
||||
if (moderationError instanceof HttpsError) throw moderationError;
|
||||
console.error("⚠️ Moderation check error (fail open)", 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",
|
||||
"Vérification de sécurité indisponible.",
|
||||
);
|
||||
throw new HttpsError('internal', 'Vérification de sécurité indisponible.')
|
||||
}
|
||||
|
||||
console.log("🎵 generateLyrics: Start generation", { style, emotion });
|
||||
console.log('🎵 generateLyrics: Start generation', { style, emotion })
|
||||
|
||||
// 3. Le Prompt "Hit Maker"
|
||||
const system = `
|
||||
@@ -197,27 +181,25 @@ TES RÈGLES D'OR :
|
||||
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();
|
||||
`.trim()
|
||||
|
||||
const structureTags = (
|
||||
Array.isArray(promptStructure) ? promptStructure : []
|
||||
)
|
||||
const structureTags = (Array.isArray(promptStructure) ? promptStructure : [])
|
||||
.map((part, index) => ` <SECTION ordre="${index + 1}">${part}</SECTION>`)
|
||||
.join("\n");
|
||||
.join('\n')
|
||||
|
||||
const fallbackStructureTags = [
|
||||
' <SECTION ordre="1">couplet</SECTION>',
|
||||
' <SECTION ordre="2">refrain</SECTION>',
|
||||
].join("\n");
|
||||
].join('\n')
|
||||
|
||||
const prompt = `
|
||||
<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>
|
||||
<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_IMPOSEE>
|
||||
@@ -231,106 +213,97 @@ ${structureTags || fallbackStructureTags}
|
||||
- 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();
|
||||
`.trim()
|
||||
|
||||
const lyricsSchema = z.object({
|
||||
title: z.string().describe("Titre de la chanson, court et accrocheur"),
|
||||
title: z.string().describe('Titre de la chanson, court et accrocheur'),
|
||||
lyrics: z
|
||||
.array(
|
||||
z.object({
|
||||
type: z
|
||||
.string()
|
||||
.describe(
|
||||
"Type de section (copier exactement la demande structure)",
|
||||
),
|
||||
type: z.string().describe('Type de section (copier exactement la demande structure)'),
|
||||
lyrics: z
|
||||
.string()
|
||||
.describe(
|
||||
"Les paroles. Pour les sections instrumentales, laisser vide ou décrire l'ambiance.",
|
||||
"Les paroles. Pour les sections instrumentales, laisser vide ou décrire l'ambiance."
|
||||
),
|
||||
}),
|
||||
})
|
||||
)
|
||||
.describe("La structure complète de la chanson"),
|
||||
.describe('La structure complète de la chanson'),
|
||||
lyricsDescription: z
|
||||
.string()
|
||||
.describe(
|
||||
"Un pitch de 2 phrases décrivant l'ambiance et le thème de la chanson pour Suno.",
|
||||
"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.error("❌ generateLyrics Error:", 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.",
|
||||
);
|
||||
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();
|
||||
if (normalized === "critical") return 4;
|
||||
if (normalized === "high") return 3;
|
||||
if (normalized === "medium") return 2;
|
||||
if (normalized === "low") return 1;
|
||||
return 0;
|
||||
};
|
||||
const severityScore = (severity = '') => {
|
||||
const normalized = String(severity || '').toLowerCase()
|
||||
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 = {}) => {
|
||||
const result = { ...rawResult };
|
||||
const excerpts = Array.isArray(result.excerpts) ? result.excerpts : [];
|
||||
const reasons = Array.isArray(result.reasons) ? result.reasons : [];
|
||||
const result = { ...rawResult }
|
||||
const excerpts = Array.isArray(result.excerpts) ? result.excerpts : []
|
||||
const reasons = Array.isArray(result.reasons) ? result.reasons : []
|
||||
|
||||
const highestSeverity = excerpts.reduce(
|
||||
(max, excerpt) => Math.max(max, severityScore(excerpt?.severity)),
|
||||
0,
|
||||
);
|
||||
0
|
||||
)
|
||||
const score =
|
||||
typeof result.score === "number" && !Number.isNaN(result.score)
|
||||
typeof result.score === 'number' && !Number.isNaN(result.score)
|
||||
? Math.min(Math.max(result.score, 0), 1)
|
||||
: 0;
|
||||
: 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 || "",
|
||||
),
|
||||
);
|
||||
/narrati|story|persona|fiction|metaphor|metaphore|récit|roleplay|contexte/i.test(reason || '')
|
||||
)
|
||||
|
||||
const adjustments = [];
|
||||
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) {
|
||||
// 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");
|
||||
result.blocked = false
|
||||
adjustments.push('auto-unblock-low-severity')
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.blocked && result.flagged) {
|
||||
const lowSignal = highestSeverity <= 1 && score < 0.4;
|
||||
const contextual = narrativeHint && highestSeverity <= 2 && score < 0.55;
|
||||
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");
|
||||
result.flagged = false
|
||||
adjustments.push('drop-flag-low-signal')
|
||||
} else if (contextual) {
|
||||
result.flagged = false;
|
||||
adjustments.push("drop-flag-contextual");
|
||||
result.flagged = false
|
||||
adjustments.push('drop-flag-contextual')
|
||||
}
|
||||
}
|
||||
|
||||
@@ -338,8 +311,8 @@ const softenModerationDecision = (rawResult = {}) => {
|
||||
...result,
|
||||
moderationAdjustments: adjustments,
|
||||
moderationCalibration: { highestSeverity, score },
|
||||
};
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
exports.analyseLyricsToxicity = onCall({}, async ({ data = {} }) => {
|
||||
try {
|
||||
@@ -356,93 +329,91 @@ exports.analyseLyricsToxicity = onCall({}, async ({ data = {} }) => {
|
||||
z.object({
|
||||
type: z.string().optional(),
|
||||
lyrics: z.string().optional(),
|
||||
}),
|
||||
})
|
||||
),
|
||||
])
|
||||
.describe("Paroles à analyser"),
|
||||
});
|
||||
.describe('Paroles à analyser'),
|
||||
})
|
||||
|
||||
const parsed = requestSchema.parse(data || {});
|
||||
const parsed = requestSchema.parse(data || {})
|
||||
const aiResult = await analyseLyrics({
|
||||
title: parsed.title || "",
|
||||
title: parsed.title || '',
|
||||
lyrics: parsed.lyrics,
|
||||
});
|
||||
const result = softenModerationDecision(aiResult);
|
||||
})
|
||||
const result = softenModerationDecision(aiResult)
|
||||
|
||||
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)
|
||||
.filter(([, v]) => (typeof v === 'number' ? v : 0) >= 0.5)
|
||||
.map(([k]) => k)
|
||||
.slice(0, 5);
|
||||
.slice(0, 5)
|
||||
|
||||
let errorCode = "OK";
|
||||
let message = "Analyse effectuée: aucun blocage.";
|
||||
let errorCode = 'OK'
|
||||
let message = 'Analyse effectuée: aucun blocage.'
|
||||
|
||||
if (result.blocked) {
|
||||
errorCode = "TOXIC_CONTENT_BLOCKED";
|
||||
message = `Contenu bloqué par sécurité.`;
|
||||
errorCode = 'TOXIC_CONTENT_BLOCKED'
|
||||
message = `Contenu bloqué par sécurité.`
|
||||
} else if (result.flagged) {
|
||||
errorCode = "TOXIC_CONTENT_FLAGGED";
|
||||
message = `Attention: contenu sensible détecté.`;
|
||||
errorCode = 'TOXIC_CONTENT_FLAGGED'
|
||||
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);
|
||||
console.error('analyseLyricsToxicity failed', err?.message || err)
|
||||
return {
|
||||
success: false,
|
||||
errorCode: "ANALYSE_FAILED",
|
||||
message: "Erreur analyse toxicité.",
|
||||
};
|
||||
errorCode: 'ANALYSE_FAILED',
|
||||
message: 'Erreur analyse toxicité.',
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
|
||||
// --- SUNO TIMESTAMPS (EXISTANT) ---
|
||||
|
||||
async function getSunoTimestamps(projectId) {
|
||||
try {
|
||||
if (!projectId || typeof projectId !== "string")
|
||||
throw new Error("projectId invalide");
|
||||
if (!projectId || typeof projectId !== 'string') throw new Error('projectId invalide')
|
||||
|
||||
const docSnap = await refList.projects.doc(projectId).get();
|
||||
if (!docSnap.exists) throw new Error("Projet introuvable");
|
||||
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");
|
||||
const { sunoTaskId, songIndex } = docSnap.data()
|
||||
if (!sunoTaskId) throw new Error('TaskId manquant')
|
||||
if (songIndex === undefined || songIndex < 0) throw new Error('musicIndex invalide')
|
||||
|
||||
console.log("🔎 getSunoTimestamps", { sunoTaskId, songIndex });
|
||||
console.log('🔎 getSunoTimestamps', { sunoTaskId, songIndex })
|
||||
|
||||
const response = await axios.post(
|
||||
`${SUNO_API_BASE}${SUNO_TIMESTAMPED_LYRICS_PATH}`,
|
||||
{ taskId: sunoTaskId, musicIndex: songIndex },
|
||||
{
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${SUNO_API_KEY}`,
|
||||
},
|
||||
timeout: 30000,
|
||||
},
|
||||
);
|
||||
}
|
||||
)
|
||||
|
||||
const dataToReturn = response.data?.data || response.data || {};
|
||||
const dataToReturn = response.data?.data || response.data || {}
|
||||
|
||||
await refList.projects.doc(projectId).set(
|
||||
{
|
||||
musicTimestamps: { [songIndex]: dataToReturn },
|
||||
updatedAt: FieldValue.serverTimestamp(),
|
||||
},
|
||||
{ merge: true },
|
||||
);
|
||||
{ merge: true }
|
||||
)
|
||||
|
||||
return { success: true };
|
||||
return { success: true }
|
||||
} catch (error) {
|
||||
console.error("❌ getSunoTimestamps Error:", error.message);
|
||||
console.error('❌ getSunoTimestamps Error:', error.message)
|
||||
return {
|
||||
success: false,
|
||||
error: { message: error.message, type: "INTERNAL_ERROR" },
|
||||
};
|
||||
error: { message: error.message, type: 'INTERNAL_ERROR' },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.getSunoTimestamps = getSunoTimestamps;
|
||||
exports.getSunoTimestamps = getSunoTimestamps
|
||||
|
||||
Reference in New Issue
Block a user