feat: filter by lang

This commit is contained in:
2026-08-11 13:19:25 +02:00
parent c8656d6273
commit 0165e8b44d
8 changed files with 139 additions and 29 deletions
+2
View File
@@ -1,5 +1,6 @@
{ {
"lockfileVersion": 1, "lockfileVersion": 1,
"configVersion": 0,
"workspaces": { "workspaces": {
"": { "": {
"name": "MusicLand", "name": "MusicLand",
@@ -110,6 +111,7 @@
}, },
"overrides": { "overrides": {
"firebase": "~9.20.0", "firebase": "~9.20.0",
"react-error-overlay": "6.0.9",
}, },
"packages": { "packages": {
"@0no-co/graphql.web": ["@0no-co/graphql.web@1.2.0", "", { "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" }, "optionalPeers": ["graphql"] }, "sha512-/1iHy9TTr63gE1YcR5idjx8UREz1s0kFhydf3bBLCXyqjhkIc6igAzTOx3zPifCwFR87tsh/4Pa9cNts6d2otw=="], "@0no-co/graphql.web": ["@0no-co/graphql.web@1.2.0", "", { "peerDependencies": { "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" }, "optionalPeers": ["graphql"] }, "sha512-/1iHy9TTr63gE1YcR5idjx8UREz1s0kFhydf3bBLCXyqjhkIc6igAzTOx3zPifCwFR87tsh/4Pa9cNts6d2otw=="],
+1 -1
View File
@@ -7,7 +7,7 @@ const { setTimeout } = require('timers/promises')
// --- CONFIGURATION --- // --- CONFIGURATION ---
// Plus intelligente que le Flash original, ultra rapide, et stable sur l'API. // Plus intelligente que le Flash original, ultra rapide, et stable sur l'API.
const TEXT_MODEL_NAME = 'gemini-3.5-flash' const TEXT_MODEL_NAME = 'gemini-flash-latest'
const IMAGE_MODEL_NAME = 'gemini-3-pro-image' const IMAGE_MODEL_NAME = 'gemini-3-pro-image'
// --- SINGLETON PATTERN (WARM START) --- // --- SINGLETON PATTERN (WARM START) ---
+13
View File
@@ -140,6 +140,7 @@ const buildModerationBrief = ({
exports.generateLyrics = onCall({ secrets: [GEMINI_API_KEY] }, async ({ auth = {}, data = {} }) => { exports.generateLyrics = onCall({ secrets: [GEMINI_API_KEY] }, async ({ auth = {}, data = {} }) => {
try { try {
const { const {
lang: rawLang,
objective = '', objective = '',
context = '', context = '',
style = '', style = '',
@@ -148,6 +149,15 @@ exports.generateLyrics = onCall({ secrets: [GEMINI_API_KEY] }, async ({ auth = {
structure: rawStructure = ['couplet', 'refrain', 'couplet', 'refrain'], structure: rawStructure = ['couplet', 'refrain', 'couplet', 'refrain'],
rhymes = '', rhymes = '',
} = data } = data
if (!['fr', 'en'].includes(rawLang)) {
throw new HttpsError('invalid-argument', 'La langue doit être "fr" ou "en".')
}
const lang = rawLang
const languageLabel = lang === 'en' ? 'English' : 'français'
const languageInstruction =
lang === 'en'
? 'Write the title, lyrics and description in English.'
: 'Rédige le titre, les paroles et la description en français.'
const rhymesInstruction = resolveRhymesInstruction(rhymes) const rhymesInstruction = resolveRhymesInstruction(rhymes)
// 1. Préparation Structure // 1. Préparation Structure
@@ -201,6 +211,7 @@ Tu es le meilleur parolier musical actuel, expert en "Songwriting" pour les IA g
Ta spécialité est la **Prosodie** (le rythme naturel des mots) et l'impact émotionnel. Ta spécialité est la **Prosodie** (le rythme naturel des mots) et l'impact émotionnel.
TES RÈGLES D'OR : TES RÈGLES D'OR :
0. **LANGUE** : ${languageInstruction}
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. 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** : Suis strictement <TYPE_DE_RIMES> du brief. Si "Sans rimes" ou "Mélange des deux" est demandé, respecte l'absence/alternance de rimes. Si rien n'est indiqué, fais des rimes riches et évite les rimes faciles (amour/toujours). 2. **RIMES** : Suis strictement <TYPE_DE_RIMES> du brief. Si "Sans rimes" ou "Mélange des deux" est demandé, respecte l'absence/alternance de rimes. Si rien n'est indiqué, fais des rimes riches et évite les rimes faciles (amour/toujours).
3. **SHOW, DON'T TELL** : N'écris pas "Je suis triste", écris "La pluie brouille mes carreaux". Utilise des détails sensoriels. 3. **SHOW, DON'T TELL** : N'écris pas "Je suis triste", écris "La pluie brouille mes carreaux". Utilise des détails sensoriels.
@@ -224,6 +235,7 @@ Retourne UNIQUEMENT un JSON valide.
const prompt = ` const prompt = `
<BRIEF_CREATIF> <BRIEF_CREATIF>
<LANGUE>${languageLabel}</LANGUE>
<OBJECTIF>${objective || 'Créer une chanson mémorable'}</OBJECTIF> <OBJECTIF>${objective || 'Créer une chanson mémorable'}</OBJECTIF>
<CONTEXTE>${context || 'Libre interprétation'}</CONTEXTE> <CONTEXTE>${context || 'Libre interprétation'}</CONTEXTE>
<EMOTION_DOMINANTE>${emotion || 'Intense'}</EMOTION_DOMINANTE> <EMOTION_DOMINANTE>${emotion || 'Intense'}</EMOTION_DOMINANTE>
@@ -237,6 +249,7 @@ ${structureTags || fallbackStructureTags}
</STRUCTURE_IMPOSEE> </STRUCTURE_IMPOSEE>
<CONSIGNES_GENERATION> <CONSIGNES_GENERATION>
0. ${languageInstruction}
1. Génère un **Titre** percutant (moins de 6 mots). 1. Génère un **Titre** percutant (moins de 6 mots).
2. Pour chaque section <SECTION>, écris le contenu adapté : 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 "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)".
+14 -5
View File
@@ -101,6 +101,9 @@ const ensureUserDocument = async (user, overrides = {}, isNewUser = false) => {
typeof overrides?.lastName === 'string' ? overrides.lastName.trim() : null typeof overrides?.lastName === 'string' ? overrides.lastName.trim() : null
const overrideDisplayName = const overrideDisplayName =
typeof overrides?.displayName === 'string' ? overrides.displayName.trim() : null typeof overrides?.displayName === 'string' ? overrides.displayName.trim() : null
const overridePreferredLanguage = ['fr', 'en'].includes(overrides?.preferredLanguage)
? overrides.preferredLanguage
: null
const { firstName: nameFromDisplay, lastName: lastFromDisplay } = splitDisplayName( const { firstName: nameFromDisplay, lastName: lastFromDisplay } = splitDisplayName(
user.displayName user.displayName
@@ -124,6 +127,10 @@ const ensureUserDocument = async (user, overrides = {}, isNewUser = false) => {
payload.lastName = lastFromDisplay payload.lastName = lastFromDisplay
} }
if (overridePreferredLanguage) {
payload.preferredLanguage = overridePreferredLanguage
}
await docRef.set(payload, { merge: true }) await docRef.set(payload, { merge: true })
return { return {
isNewUser, isNewUser,
@@ -138,7 +145,7 @@ const ensureUserDocument = async (user, overrides = {}, isNewUser = false) => {
} }
} }
const useSocialAuth = ({ onSuccess } = {}) => { const useSocialAuth = ({ onSuccess, preferredLanguage = null } = {}) => {
const [, setTooltip] = useGlobal('_tooltip') const [, setTooltip] = useGlobal('_tooltip')
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const [isAppleSignInAvailable, setIsAppleSignInAvailable] = useState(false) const [isAppleSignInAvailable, setIsAppleSignInAvailable] = useState(false)
@@ -235,7 +242,7 @@ const useSocialAuth = ({ onSuccess } = {}) => {
const userCredential = await firebase.auth().signInWithCredential(credential) const userCredential = await firebase.auth().signInWithCredential(credential)
const authResult = await ensureUserDocument( const authResult = await ensureUserDocument(
userCredential?.user, userCredential?.user,
{}, { preferredLanguage },
userCredential?.additionalUserInfo?.isNewUser === true userCredential?.additionalUserInfo?.isNewUser === true
) )
if (typeof onSuccess === 'function') { if (typeof onSuccess === 'function') {
@@ -253,7 +260,7 @@ const useSocialAuth = ({ onSuccess } = {}) => {
setIsLoading(false) setIsLoading(false)
} }
}, },
[onSuccess, redirectUri, request, setTooltip] [onSuccess, preferredLanguage, redirectUri, request, setTooltip]
) )
useEffect(() => { useEffect(() => {
@@ -316,7 +323,7 @@ const useSocialAuth = ({ onSuccess } = {}) => {
const userCredential = await firebase.auth().signInWithPopup(provider) const userCredential = await firebase.auth().signInWithPopup(provider)
const authResult = await ensureUserDocument( const authResult = await ensureUserDocument(
userCredential?.user, userCredential?.user,
{}, { preferredLanguage },
userCredential?.additionalUserInfo?.isNewUser === true userCredential?.additionalUserInfo?.isNewUser === true
) )
if (typeof onSuccess === 'function') { if (typeof onSuccess === 'function') {
@@ -370,6 +377,7 @@ const useSocialAuth = ({ onSuccess } = {}) => {
isLoading, isLoading,
isWeb, isWeb,
onSuccess, onSuccess,
preferredLanguage,
promptAsync, promptAsync,
setTooltip, setTooltip,
]) ])
@@ -422,6 +430,7 @@ const useSocialAuth = ({ onSuccess } = {}) => {
{ {
firstName, firstName,
lastName, lastName,
preferredLanguage,
displayName: displayName:
userCredential?.user?.displayName || userCredential?.user?.displayName ||
[firstName, lastName].filter(Boolean).join(' ') || [firstName, lastName].filter(Boolean).join(' ') ||
@@ -447,7 +456,7 @@ const useSocialAuth = ({ onSuccess } = {}) => {
setTooltip({ text: message, type: 'error' }) setTooltip({ text: message, type: 'error' })
setIsLoading(false) setIsLoading(false)
} }
}, [isAppleSignInAvailable, isLoading, onSuccess, setTooltip]) }, [isAppleSignInAvailable, isLoading, onSuccess, preferredLanguage, setTooltip])
return { return {
signInWithGoogle, signInWithGoogle,
+2
View File
@@ -222,9 +222,11 @@ export default ({ children }) => {
try { try {
await setIsLoading(true) await setIsLoading(true)
const artistDisplayName = getUserPreferredArtistName(currentUserDoc || currentUserData || {}) const artistDisplayName = getUserPreferredArtistName(currentUserDoc || currentUserData || {})
const lang = currentUserDoc?.preferredLanguage === 'en' ? 'en' : 'fr'
const payload = { const payload = {
userId: currentUID || null, userId: currentUID || null,
userName: artistDisplayName, userName: artistDisplayName,
lang,
createdAt: firebase.firestore.FieldValue.serverTimestamp(), createdAt: firebase.firestore.FieldValue.serverTimestamp(),
updatedAt: firebase.firestore.FieldValue.serverTimestamp(), updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
} }
+89 -21
View File
@@ -1,13 +1,5 @@
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { import { FlatList, Image, Platform, ScrollView, StyleSheet, Text, View } from 'react-native'
FlatList,
Image,
Platform,
ScrollView,
StyleSheet,
Text,
View,
} from 'react-native'
import { BlurView } from 'expo-blur' import { BlurView } from 'expo-blur'
import { responsiveHeight } from 'react-native-responsive-dimensions' import { responsiveHeight } from 'react-native-responsive-dimensions'
import { background, icons } from '../../assets' import { background, icons } from '../../assets'
@@ -29,6 +21,30 @@ import SongCard from './components/SongCard'
import ClubCard from '../Home/components/ClubCard' import ClubCard from '../Home/components/ClubCard'
const allowedPremiumLevels = new Set(['starter', 'pro', 'premium']) const allowedPremiumLevels = new Set(['starter', 'pro', 'premium'])
const languageOptions = [
{ value: 'all', label: 'Toutes' },
{ value: 'fr', label: 'Français' },
{ value: 'en', label: 'English' },
]
const StreamingLanguageFilter = ({ selectedLanguage, onSelectLanguage }) => (
<View style={styles.languageFilter}>
<Text style={styles.languageFilterLabel}>Langue des musiques</Text>
<View style={styles.languageFilterButtons}>
{languageOptions.map((option) => (
<BorderGradientButton
key={option.value}
title={option.label}
onPress={() => onSelectLanguage(option.value)}
tint={selectedLanguage === option.value ? 'light' : 'dark'}
containerStyle={styles.languageFilterButton}
titleStyle={styles.languageFilterButtonText}
size="small"
/>
))}
</View>
</View>
)
const normalizePremiumLevel = (value) => { const normalizePremiumLevel = (value) => {
if (typeof value !== 'string') { if (typeof value !== 'string') {
@@ -48,13 +64,19 @@ const chunkArray = (items = [], size = 10) => {
const HitParade = () => { const HitParade = () => {
const [selectedCategory, setSelectedCategory] = useState('Chansons') const [selectedCategory, setSelectedCategory] = useState('Chansons')
const [selectedLanguage, setSelectedLanguage] = useState('all')
const navigateToMusicDetails = useNavigateToMusicDetails() const navigateToMusicDetails = useNavigateToMusicDetails()
const allSongsRef = useMemo(() => {
const songsRef = projectsRef.where('hasSong', '==', true)
return selectedLanguage === 'all' ? songsRef : songsRef.where('lang', '==', selectedLanguage)
}, [selectedLanguage])
const { data: allSongs } = useDataFromRef({ const { data: allSongs } = useDataFromRef({
ref: projectsRef.where('hasSong', '==', true), ref: allSongsRef,
format: (docs) => docs.filter((item) => item?.hasPlayback !== true), format: (docs) => docs.filter((item) => item?.hasPlayback !== true),
simpleRef: false, simpleRef: false,
listener: true, listener: true,
condition: true, condition: true,
refreshArray: [selectedLanguage],
}) })
const { data: topMonthSongs } = useDataFromRef({ const { data: topMonthSongs } = useDataFromRef({
@@ -62,7 +84,8 @@ const HitParade = () => {
format: (docs) => docs.filter((item) => item?.hasPlayback !== true), format: (docs) => docs.filter((item) => item?.hasPlayback !== true),
simpleRef: false, simpleRef: false,
listener: false, listener: false,
condition: true, condition: selectedLanguage === 'all',
refreshArray: [selectedLanguage],
}) })
const { data: allTimeSongs } = useDataFromRef({ const { data: allTimeSongs } = useDataFromRef({
@@ -70,20 +93,31 @@ const HitParade = () => {
format: (docs) => docs.filter((item) => item?.hasPlayback !== true), format: (docs) => docs.filter((item) => item?.hasPlayback !== true),
simpleRef: false, simpleRef: false,
listener: false, listener: false,
condition: true, condition: selectedLanguage === 'all',
refreshArray: [selectedLanguage],
}) })
const songsList = useMemo(() => (Array.isArray(allSongs) ? allSongs : []), [allSongs]) const songsList = useMemo(() => (Array.isArray(allSongs) ? allSongs : []), [allSongs])
const topMonthList = useMemo( const topMonthList = useMemo(() => {
() => (Array.isArray(topMonthSongs) ? topMonthSongs : []), if (selectedLanguage === 'all') {
[topMonthSongs] return Array.isArray(topMonthSongs) ? topMonthSongs : []
) }
return [...songsList]
.filter((item) => typeof item?.monthViews === 'number' && item.monthViews > 0)
.sort((a, b) => b.monthViews - a.monthViews)
.slice(0, 20)
}, [selectedLanguage, songsList, topMonthSongs])
const allTimeList = useMemo( const allTimeList = useMemo(() => {
() => (Array.isArray(allTimeSongs) ? allTimeSongs : []), if (selectedLanguage === 'all') {
[allTimeSongs] return Array.isArray(allTimeSongs) ? allTimeSongs : []
) }
return [...songsList]
.filter((item) => typeof item?.views === 'number' && item.views > 0)
.sort((a, b) => b.views - a.views)
.slice(0, 20)
}, [allTimeSongs, selectedLanguage, songsList])
// === RENDUS PAR PLATEFORME === // === RENDUS PAR PLATEFORME ===
const renderSongsMobile = () => ( const renderSongsMobile = () => (
@@ -254,7 +288,12 @@ const HitParade = () => {
[setSearchSelected] [setSearchSelected]
) )
const musicResults = useMemo(() => (Array.isArray(musics) ? musics : []), [musics]) const musicResults = useMemo(() => {
const results = Array.isArray(musics) ? musics : []
return selectedLanguage === 'all'
? results
: results.filter((project) => project?.lang === selectedLanguage)
}, [musics, selectedLanguage])
const [creatorsById, setCreatorsById] = useState({}) const [creatorsById, setCreatorsById] = useState({})
@@ -632,6 +671,10 @@ const HitParade = () => {
style={{ alignSelf: 'center', height: 150, resizeMode: 'contain' }} style={{ alignSelf: 'center', height: 150, resizeMode: 'contain' }}
/> */} /> */}
<ClubCard /> <ClubCard />
<StreamingLanguageFilter
selectedLanguage={selectedLanguage}
onSelectLanguage={setSelectedLanguage}
/>
{isWeb && ( {isWeb && (
<View <View
@@ -724,3 +767,28 @@ const HitParade = () => {
} }
export default HitParade export default HitParade
const styles = StyleSheet.create({
languageFilter: {
width: '100%',
maxWidth: 620,
alignSelf: 'center',
gap: 8,
},
languageFilterLabel: {
color: Palette.white,
fontSize: 13,
fontFamily: FONT_FAMILY.InterSemiBold,
textAlign: 'center',
},
languageFilterButtons: {
flexDirection: 'row',
gap: 10,
},
languageFilterButton: {
flex: 1,
},
languageFilterButtonText: {
fontSize: 13,
},
})
+1
View File
@@ -56,6 +56,7 @@ const Register = () => {
isLoading: socialLoading, isLoading: socialLoading,
} = useSocialAuth({ } = useSocialAuth({
onSuccess: afterSocialAuth, onSuccess: afterSocialAuth,
preferredLanguage,
}) })
const isBusy = socialLoading const isBusy = socialLoading
+17 -2
View File
@@ -29,7 +29,8 @@ const CreatingLyrics = ({ active, config, selections, onErrorRedirect }) => {
const [error, setError] = useState(null) const [error, setError] = useState(null)
const hasShownErrorAlert = useRef(false) const hasShownErrorAlert = useRef(false)
const { setIsLoading } = useMinuit() const { setIsLoading } = useMinuit()
const { updateProjectData } = useUser() const { selectedProject, updateProjectData } = useUser()
const projectLang = selectedProject?.lang === 'en' ? 'en' : 'fr'
const progressValue = Math.max(0, Math.min(100, Math.round(progress))) const progressValue = Math.max(0, Math.min(100, Math.round(progress)))
// Reset when becomes active // Reset when becomes active
@@ -74,6 +75,7 @@ const CreatingLyrics = ({ active, config, selections, onErrorRedirect }) => {
const sanitizedStructure = sanitizeStructureList(config?.structure, CUSTOM_SANITIZE_OPTIONS) const sanitizedStructure = sanitizeStructureList(config?.structure, CUSTOM_SANITIZE_OPTIONS)
const callable = firebase.functions().httpsCallable('lyrics-generateLyrics') const callable = firebase.functions().httpsCallable('lyrics-generateLyrics')
const { data } = await callable({ const { data } = await callable({
lang: projectLang,
objective: config?.objective, objective: config?.objective,
context: config?.context, context: config?.context,
emotion: config?.emotion, emotion: config?.emotion,
@@ -188,6 +190,7 @@ const CreatingLyrics = ({ active, config, selections, onErrorRedirect }) => {
} }
await updateProjectData({ await updateProjectData({
lang: projectLang,
title: result?.title || '', title: result?.title || '',
lyrics: Array.isArray(result?.lyrics) ? result.lyrics : [], lyrics: Array.isArray(result?.lyrics) ? result.lyrics : [],
description: result?.lyricsDescription, description: result?.lyricsDescription,
@@ -206,7 +209,18 @@ const CreatingLyrics = ({ active, config, selections, onErrorRedirect }) => {
if (active && result && progress >= 100 && !saved && !error) { if (active && result && progress >= 100 && !saved && !error) {
autoSaveAndGo() autoSaveAndGo()
} }
}, [active, result, progress, saved, error, setIsLoading, updateProjectData, config, selections]) }, [
active,
result,
progress,
saved,
error,
setIsLoading,
updateProjectData,
config,
selections,
projectLang,
])
useEffect(() => { useEffect(() => {
if (error && !hasShownErrorAlert.current) { if (error && !hasShownErrorAlert.current) {
@@ -303,6 +317,7 @@ const CreatingLyrics = ({ active, config, selections, onErrorRedirect }) => {
await setIsLoading(true) await setIsLoading(true)
console.log('📄 [CreatingLyrics] Consultation manuelle du texte') console.log('📄 [CreatingLyrics] Consultation manuelle du texte')
await updateProjectData({ await updateProjectData({
lang: projectLang,
title: result?.title || '', title: result?.title || '',
lyrics: Array.isArray(result?.lyrics) ? result.lyrics : [], lyrics: Array.isArray(result?.lyrics) ? result.lyrics : [],
config: config || null, config: config || null,