396 lines
10 KiB
JavaScript
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;
|
|
};
|