const START_CASE_REGEX = /[_\-\s]+/g; const toStartCase = (value) => { const str = String(value || "") .replace(START_CASE_REGEX, " ") .trim(); if (!str) return ""; return str .split(" ") .filter(Boolean) .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .join(" "); }; export const STRUCTURE_SEGMENTS = { couplet: { label: "Couplet", promptLabel: "Couplet", showIndex: true, allowMultiple: true, optional: false, inputHeight: 225, requiresLyrics: true, aliases: ["vers", "verse", "couplet"], }, refrain: { label: "Refrain", promptLabel: "Refrain", showIndex: true, allowMultiple: true, optional: false, inputHeight: 170, requiresLyrics: true, aliases: ["chorus", "refrain"], }, short_intro: { label: "Introduction instrumentale courte", promptLabel: "Introduction instrumentale courte", showIndex: false, allowMultiple: false, optional: true, inputHeight: 150, exclusiveGroup: "intro", requiresLyrics: false, aliases: [ "introduction instrumentale courte", "intro courte", "intro instrumentale courte", "short intro", ], }, long_intro: { label: "Introduction instrumentale longue", promptLabel: "Introduction instrumentale longue", showIndex: false, allowMultiple: false, optional: true, inputHeight: 150, exclusiveGroup: "intro", requiresLyrics: false, aliases: [ "introduction instrumentale longue", "intro longue", "long intro", "intro instrumentale longue", ], }, pre_refrain_instrumental: { label: "Pré-refrain instrumental", promptLabel: "Pré-refrain instrumental", showIndex: false, allowMultiple: false, optional: true, inputHeight: 150, exclusiveGroup: "pre_chorus", requiresLyrics: false, aliases: [], }, pont: { label: "Pont", promptLabel: "Pont", showIndex: false, allowMultiple: true, optional: true, inputHeight: 225, requiresLyrics: false, aliases: [], }, solo_de_guitare: { label: "Solo de guitare", promptLabel: "Solo de guitare", showIndex: true, allowMultiple: true, optional: true, inputHeight: 150, requiresLyrics: false, aliases: [], }, solo_de_guitare_electrique: { label: "Solo de guitare électrique", promptLabel: "Solo de guitare électrique", showIndex: true, allowMultiple: true, optional: true, inputHeight: 150, requiresLyrics: false, aliases: [], }, solo_de_batterie: { label: "Solo de batterie", promptLabel: "Solo de batterie", showIndex: true, allowMultiple: true, optional: true, inputHeight: 150, requiresLyrics: false, aliases: [], }, solo_de_saxophone: { label: "Solo de saxophone", promptLabel: "Solo de saxophone", showIndex: true, allowMultiple: true, optional: true, inputHeight: 150, requiresLyrics: false, aliases: [], }, solo_de_violon: { label: "Solo de violon", promptLabel: "Solo de violon", showIndex: true, allowMultiple: true, optional: true, inputHeight: 150, requiresLyrics: false, aliases: [], }, break: { label: "Break", promptLabel: "Break", showIndex: true, allowMultiple: true, optional: true, inputHeight: 150, requiresLyrics: false, aliases: [], }, interlude: { label: "Interlude", promptLabel: "Interlude", showIndex: true, allowMultiple: true, optional: true, inputHeight: 150, requiresLyrics: false, aliases: [], }, interlude_melodique: { label: "Interlude mélodique", promptLabel: "Interlude mélodique", showIndex: true, allowMultiple: true, optional: true, inputHeight: 150, requiresLyrics: false, aliases: [], }, final_apogee: { label: "Final apogée", promptLabel: "Final apogée", showIndex: false, allowMultiple: false, optional: true, inputHeight: 170, exclusiveGroup: "outro", requiresLyrics: false, aliases: [], }, arret_net: { label: "Arrêt net", promptLabel: "Arrêt net", showIndex: false, allowMultiple: false, optional: true, inputHeight: 150, exclusiveGroup: "outro", requiresLyrics: false, aliases: [], }, fade_out: { label: "Fade out", promptLabel: "Fade out", showIndex: false, allowMultiple: false, optional: true, inputHeight: 150, exclusiveGroup: "outro", requiresLyrics: false, aliases: [], }, transition_douce: { label: "Transition douce vers le silence", promptLabel: "Transition douce", showIndex: false, allowMultiple: false, optional: true, inputHeight: 150, exclusiveGroup: "outro", requiresLyrics: false, aliases: [], }, }; const ALIAS_TO_KEY = Object.entries(STRUCTURE_SEGMENTS).reduce( (acc, [key, meta]) => { const aliases = Array.isArray(meta.aliases) ? meta.aliases : []; aliases.forEach((alias) => { acc[String(alias).toLowerCase()] = key; }); return acc; }, {} ); const LEGACY_STRUCTURE_ALIASES = { pre_chorus: "pre_refrain_instrumental", "pré-refrain": "pre_refrain_instrumental", "pre-refrain": "pre_refrain_instrumental", "pre_refrain": "pre_refrain_instrumental", prechorus: "pre_refrain_instrumental", "pre chorus": "pre_refrain_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", }; Object.entries(LEGACY_STRUCTURE_ALIASES).forEach(([alias, target]) => { const key = String(alias).toLowerCase(); ALIAS_TO_KEY[key] = target; }); const sanitizeToken = (value) => String(value || "") .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, "_") .replace(/^_+|_+$/g, ""); export const getSegmentMeta = (type) => { const key = String(type || "").toLowerCase(); return STRUCTURE_SEGMENTS[key] || null; }; export const segmentRequiresLyrics = (type) => { const meta = getSegmentMeta(type); if (!meta) return true; return meta.requiresLyrics !== false; }; export const OPTIONAL_STRUCTURE_SEGMENTS = Object.keys( STRUCTURE_SEGMENTS ).filter((key) => STRUCTURE_SEGMENTS[key]?.optional); export const normalizeStructureType = (value) => { const raw = String(value || "").trim(); if (!raw) return ""; const lower = raw.toLowerCase(); if (STRUCTURE_SEGMENTS[lower]) return lower; if (ALIAS_TO_KEY[lower]) return ALIAS_TO_KEY[lower]; const sanitized = sanitizeToken(raw); if (STRUCTURE_SEGMENTS[sanitized]) return sanitized; if (ALIAS_TO_KEY[sanitized]) return ALIAS_TO_KEY[sanitized]; return sanitized; }; export const sanitizeStructureList = (structure = []) => { if (!Array.isArray(structure)) return []; const output = []; let shouldInjectInstrumentalPreChorus = false; structure.forEach((segment) => { const type = normalizeStructureType(segment); if (!type) return; const meta = getSegmentMeta(type); if (meta) { if (meta.allowMultiple === false) { const alreadyIncluded = output.some((item) => item === type); if (alreadyIncluded) return; } if (meta.exclusiveGroup) { const previousIndex = output.findIndex((item) => { const existingMeta = getSegmentMeta(item); return existingMeta?.exclusiveGroup === meta.exclusiveGroup; }); if (previousIndex !== -1) { output.splice(previousIndex, 1); } } } if (type === "pre_refrain_instrumental") { shouldInjectInstrumentalPreChorus = true; return; } output.push(type); }); let result = output; if (shouldInjectInstrumentalPreChorus) { const expanded = []; let hasRefrain = false; result.forEach((item) => { if (item === "refrain") { hasRefrain = true; const previous = expanded[expanded.length - 1]; if (previous !== "pre_refrain_instrumental") { expanded.push("pre_refrain_instrumental"); } } expanded.push(item); }); if (!hasRefrain) { expanded.push("pre_refrain_instrumental"); } result = expanded; } const introIndex = result.findIndex( (item) => item === "short_intro" || item === "long_intro" ); if (introIndex > 0) { const [intro] = result.splice(introIndex, 1); result.unshift(intro); } return result; }; export const formatStructureLabel = (type, occurrence = 1) => { const normalized = normalizeStructureType(type); const meta = getSegmentMeta(normalized); const baseLabel = meta?.label || toStartCase(normalized || type); const showIndex = meta?.showIndex === true; if (showIndex) return `${baseLabel} ${occurrence}`; if (!meta && occurrence > 1) return `${baseLabel} ${occurrence}`; return baseLabel; }; export const getStructureInputHeight = (type) => { const normalized = normalizeStructureType(type); const meta = getSegmentMeta(normalized); return typeof meta?.inputHeight === "number" ? meta.inputHeight : undefined; }; export const getPromptLabelForStructure = (type) => { const normalized = normalizeStructureType(type); const meta = getSegmentMeta(normalized); if (meta?.promptLabel) return meta.promptLabel; if (meta?.label) return meta.label; return toStartCase(normalized || type); }; export const isOptionalStructureType = (type) => { const normalized = normalizeStructureType(type); const meta = getSegmentMeta(normalized); return !!meta?.optional; }; export const arraysAreSame = (a = [], b = []) => { if (a === b) return true; if (!Array.isArray(a) || !Array.isArray(b)) return false; if (a.length !== b.length) return false; for (let i = 0; i < a.length; i += 1) { if (a[i] !== b[i]) return false; } return true; };