new song structure

This commit is contained in:
2025-10-21 11:38:48 +02:00
parent 59328aaaed
commit 3a5d449424
11 changed files with 807 additions and 175 deletions
+191
View File
@@ -0,0 +1,191 @@
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,
aliases: ["vers", "verse", "couplet"],
},
refrain: {
label: "Refrain",
promptLabel: "Refrain",
showIndex: true,
allowMultiple: true,
optional: false,
inputHeight: 170,
aliases: ["chorus", "refrain"],
},
short_intro: {
label: "Introduction instrumentale courte",
promptLabel: "Introduction instrumentale courte",
showIndex: false,
allowMultiple: false,
optional: true,
inputHeight: 150,
exclusiveGroup: "intro",
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",
aliases: [
"introduction instrumentale longue",
"intro longue",
"long intro",
"intro instrumentale longue",
],
},
pre_chorus: {
label: "Pré-refrain",
promptLabel: "Pré-refrain",
showIndex: false,
allowMultiple: false,
optional: true,
inputHeight: 150,
aliases: [
"pré-refrain",
"pre-refrain",
"prechorus",
"pre chorus",
"pré chorus",
],
},
};
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 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 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 = [];
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);
}
}
}
output.push(type);
});
return output;
};
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;
};