const { onCall } = 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 admin = require("firebase-admin"); const { refList } = require("../index"); exports.generateLyrics = onCall({}, async ({ auth = {}, data = {} }) => { try { const { objective = "", context = "", style = "", audience = "", emotion = "", structure = ["couplet", "refrain", "couplet", "refrain"], rhymes = "", } = data; console.log("Function generate lyrics start with data", data); const system = ` Tu es un parolier expert en chanson française moderne et populaire. Tu écris des chansons marquantes, accessibles, sur des thématiques ` + `d'innovation, d'esprit d'équipe et de créativité digitale. Ton ` + `écriture doit toucher à la fois l'humain, le collectif et la ` + `passion d'entreprendre. Tu dois générer un titre accrocheur et ` + `mémorable pour la chanson en plus des paroles. Respecte ` + `précisément la structure demandée et fournis le résultat dans un ` + `format JSON clair, respectant strictement le schéma demandé. `.trim(); // Construction du prompt utilisateur const prompt = ` Objectif : ${objective} Contexte : ${context} Émotion : ${emotion} Style musical : ${style} Audience cible : ${audience} Structure de la chanson : ${structure.join(" => ")} Schéma de rimes : ${rhymes} Génère un titre accrocheur et les paroles de la chanson en suivant ` + `strictement cette structure. Le titre doit être court, ` + `mémorable et refléter l'esprit de la musique. Pour chaque ` + `partie ("couplet" ou "refrain"), écris 4 à 16 lignes adaptées, ` + `expressives et cohérentes avec le style et l'émotion souhaitée. `.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 .array( z.object({ type: z .string() .describe( 'Type de section : "couplet" ou "refrain" ' + "selon la structure", ), lyrics: z .string() .describe( "Paroles de la section, chaque ligne séparée " + "par un retour à la ligne", ), }), ) .describe( "Paroles de la chanson sous forme de tableau de sections " + "structurées.", ), 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"), }); return await generateAI({ system, prompt, schema: lyricsSchema, }); } catch (e) { console.log(e); } }); // 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 .union([ z.string(), z.object({ couplet: z.string().optional(), refrain: z.string().optional(), }), z.array( z.object({ type: z.string().optional(), lyrics: z.string().optional(), }), ), ]) .describe("Paroles à analyser (formats supportés)"), }); const parsed = requestSchema.parse(data || {}); const result = await analyseLyrics({ title: parsed.title || "", lyrics: parsed.lyrics, }); // Prépare un message lisible pour le front const highCats = Object.entries(result.categories || {}) .filter(([, v]) => (typeof v === "number" ? v : 0) >= 0.5) .map(([k]) => k) .slice(0, 5); let errorCode = "OK"; let message = "Analyse effectuée: aucun blocage."; if (result.blocked) { errorCode = "TOXIC_CONTENT_BLOCKED"; const list = highCats.length ? ` (${highCats.join(", ")})` : ""; message = `Publication bloquée: contenu interdit détecté${list}.`; } else if (result.flagged) { errorCode = "TOXIC_CONTENT_FLAGGED"; const list = highCats.length ? ` (${highCats.join(", ")})` : ""; message = `Attention: contenu potentiellement sensible détecté${list}.`; } 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.", }; } }); /** * Récupère les timestamps (aligned words) pour une génération Suno * Attend: { projectId: string } */ async function getSunoTimestamps(projectId) { try { const { sunoTaskId, songIndex } = ( await refList.projects.doc(projectId).get() ).data(); if (!sunoTaskId) { throw new Error("TaskId manquant"); } if (songIndex < 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", { sunoTaskId, songIndex, }); let response; let parsed; response = await axios.post( `${SUNO_API_BASE}${SUNO_TIMESTAMPED_LYRICS_PATH}`, { taskId: sunoTaskId, musicIndex: songIndex }, { 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); const dataToReturn = parsed?.data || parsed || {}; await refList.projects.doc(projectId).set( { musicTimestamps: { [songIndex]: dataToReturn, }, updatedAt: admin.firestore.FieldValue.serverTimestamp(), }, { merge: true }, ); console.log("💾 Timestamps sauvegardés dans Firestore", { projectId, songIndex, }); return { success: true, }; } catch (error) { console.error("❌ Erreur interne getSunoTimestamps:", error); return { success: false, error: { message: error.message, type: "INTERNAL_ERROR", }, }; } } exports.getSunoTimestamps = getSunoTimestamps;