Files
musicland/src/utils/songStructure.js
T
2025-10-22 16:32:58 +02:00

396 lines
10 KiB
JavaScript

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;
};