instrumental introduction

This commit is contained in:
2025-10-21 13:34:16 +02:00
parent 3a5d449424
commit 837a634997
6 changed files with 226 additions and 92 deletions
+17 -4
View File
@@ -39,8 +39,10 @@ import { FONT_FAMILY } from "../../styles/Fonts";
import Style, { gutters, size } from "../../styles/Style"; import Style, { gutters, size } from "../../styles/Style";
import { import {
formatStructureLabel, formatStructureLabel,
getPromptLabelForStructure,
getSegmentMeta, getSegmentMeta,
normalizeStructureType, normalizeStructureType,
segmentRequiresLyrics,
} from "../../utils/songStructure"; } from "../../utils/songStructure";
// 20 secondes // 20 secondes
@@ -333,12 +335,20 @@ const MusicDetails = ({ route }) => {
const counts = {}; const counts = {};
return project.lyrics return project.lyrics
.map((s) => { .map((s) => {
const body = (s?.lyrics || "").trim();
if (!body) return null;
const type = normalizeStructureType(s?.type); const type = normalizeStructureType(s?.type);
const requiresLyrics = segmentRequiresLyrics(type);
const body = (s?.lyrics || "").trim();
if (requiresLyrics && !body) return null;
counts[type] = (counts[type] || 0) + 1; counts[type] = (counts[type] || 0) + 1;
const label = formatStructureLabel(type, counts[type]); const label = formatStructureLabel(type, counts[type]);
return `[${label}]\n${body}`; const displayLabel =
typeof label === "string" && label.length > 0
? label
: getPromptLabelForStructure(type);
if (!requiresLyrics) {
return `[${displayLabel}] — section instrumentale`;
}
return `[${displayLabel}]\n${body}`;
}) })
.filter(Boolean) .filter(Boolean)
.join("\n\n"); .join("\n\n");
@@ -404,8 +414,11 @@ const MusicDetails = ({ route }) => {
const normalized = normalizeStructureType(type); const normalized = normalizeStructureType(type);
const meta = getSegmentMeta(normalized); const meta = getSegmentMeta(normalized);
if (meta) { if (meta) {
return formatStructureLabel(normalized, index); const label = formatStructureLabel(normalized, index);
if (label) return label;
} }
const prompt = getPromptLabelForStructure(normalized);
if (prompt) return prompt;
if (normalized === "refrain") if (normalized === "refrain")
return index > 1 ? `Refrain ${index}` : "Refrain"; return index > 1 ? `Refrain ${index}` : "Refrain";
if (normalized === "couplet") if (normalized === "couplet")
+34 -11
View File
@@ -12,7 +12,11 @@ import { navigate } from "../../navigation/NavigationService";
import { useUser } from "../../providers/UserDataProvider"; import { useUser } from "../../providers/UserDataProvider";
import { Palette, Style } from "../../styles"; import { Palette, Style } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts"; import { FONT_FAMILY } from "../../styles/Fonts";
import { sanitizeStructureList } from "../../utils/songStructure"; import {
normalizeStructureType,
sanitizeStructureList,
segmentRequiresLyrics,
} from "../../utils/songStructure";
const CreatingLyrics = ({ active, config, selections }) => { const CreatingLyrics = ({ active, config, selections }) => {
const [progress, setProgress] = useState(0); const [progress, setProgress] = useState(0);
@@ -74,21 +78,40 @@ const CreatingLyrics = ({ active, config, selections }) => {
}); });
const rawLyrics = Array.isArray(data?.lyrics) ? data.lyrics : []; const rawLyrics = Array.isArray(data?.lyrics) ? data.lyrics : [];
const normalizedLyrics = rawLyrics.map((section) => { const normalizedLyrics = rawLyrics.map((section) => {
const sanitizedType = const sanitizedType = normalizeStructureType(section?.type);
sanitizeStructureList([section?.type])[0] || section?.type || "";
return { return {
...section, ...section,
type: sanitizedType, type: sanitizedType,
lyrics: section?.lyrics || "",
}; };
}); });
const alignedLyrics =
sanitizedStructure.length > 0 && const remaining = [...normalizedLyrics];
sanitizedStructure.length === normalizedLyrics.length const takeForType = (type, { fallback = true } = {}) => {
? normalizedLyrics.map((section, index) => ({ const index = remaining.findIndex((item) => item.type === type);
...section, if (index !== -1) {
type: sanitizedStructure[index], return remaining.splice(index, 1)[0];
})) }
: normalizedLyrics; if (!fallback) return null;
return remaining.shift() || null;
};
const structuredLyrics = sanitizedStructure.length
? sanitizedStructure.map((rawType) => {
const type = normalizeStructureType(rawType);
if (!segmentRequiresLyrics(type)) {
const match = takeForType(type, { fallback: false });
return { type, lyrics: match?.lyrics || "" };
}
const match = takeForType(type, { fallback: true });
return {
type,
lyrics: match?.lyrics || "",
};
})
: normalizedLyrics;
const alignedLyrics = structuredLyrics.concat(remaining);
const processedResult = { const processedResult = {
...data, ...data,
lyrics: alignedLyrics, lyrics: alignedLyrics,
+21 -32
View File
@@ -23,15 +23,14 @@ import {
import CreateLyricsHeader from "./components/CreateLyricsHeader"; import CreateLyricsHeader from "./components/CreateLyricsHeader";
const randomSuffix = () => Math.random().toString(36).slice(2, 8); const randomSuffix = () => Math.random().toString(36).slice(2, 8);
const createItemId = (type) => `${type}-${Date.now()}-${randomSuffix()}`;
const createSortableItem = (type, index = 0) => ({ const createSortableItem = (type, index = 0) => ({
id: `${type}-${index}-${randomSuffix()}`, id: `${type}-${index}-${randomSuffix()}`,
type, type,
value: type, value: type,
}); });
const buildInitialItems = (structure = []) => const buildSortableItems = (values = []) =>
sanitizeStructureList(structure).map((type, index) => sanitizeStructureList(values).map((type, index) =>
createSortableItem(type, index) createSortableItem(type, index)
); );
@@ -42,24 +41,17 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
[baseStructure] [baseStructure]
); );
const [sortableItems, setSortableItems] = useState(() => const [sortableItems, setSortableItems] = useState(() =>
buildInitialItems(sanitizedBase) buildSortableItems(sanitizedBase)
); );
useEffect(() => { useEffect(() => {
setSortableItems((prev) => { setSortableItems(buildSortableItems(sanitizedBase));
const prevTypes = prev.map((item) => item.type);
if (
prevTypes.length === sanitizedBase.length &&
prevTypes.every((type, index) => type === sanitizedBase[index])
) {
return prev;
}
return buildInitialItems(sanitizedBase);
});
}, [sanitizedBase]); }, [sanitizedBase]);
useEffect(() => { useEffect(() => {
const values = sortableItems.map((item) => item.type).filter(Boolean); const values = sortableItems
.map((item) => item?.type || item?.value)
.filter(Boolean);
onChange?.(values); onChange?.(values);
}, [sortableItems, onChange]); }, [sortableItems, onChange]);
@@ -99,20 +91,21 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
const toggleOptionalSegment = (type) => { const toggleOptionalSegment = (type) => {
setSortableItems((prev) => { setSortableItems((prev) => {
const existsIndex = prev.findIndex((item) => item.type === type); const currentValues = prev.map((item) => item.type || item.value);
if (existsIndex !== -1) { if (currentValues.includes(type)) {
return prev.filter((item) => item.type !== type); const filtered = currentValues.filter((value) => value !== type);
return buildSortableItems(filtered);
} }
const meta = getSegmentMeta(type); const meta = getSegmentMeta(type);
let next = [...prev]; let nextValues = [...currentValues];
if (meta?.exclusiveGroup) { if (meta?.exclusiveGroup) {
next = next.filter((item) => { nextValues = nextValues.filter((value) => {
const itemMeta = getSegmentMeta(item.type || item.value); const itemMeta = getSegmentMeta(value);
return itemMeta?.exclusiveGroup !== meta.exclusiveGroup; return itemMeta?.exclusiveGroup !== meta.exclusiveGroup;
}); });
} }
next.push(createSortableItem(type, next.length)); nextValues.push(type);
return next; return buildSortableItems(nextValues);
}); });
}; };
@@ -182,15 +175,11 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
customHandle customHandle
scrollableRef={scrollRef} scrollableRef={scrollRef}
onDragEnd={({ data: newData }) => { onDragEnd={({ data: newData }) => {
const next = (newData || []).map((it, index) => { const values = (newData || [])
const nextType = it?.type || it?.value; .map((it) => it?.type || it?.value)
return { .filter(Boolean);
id: it?.id || createItemId(nextType || `section-${index}`), const sanitized = sanitizeStructureList(values);
type: nextType, setSortableItems(buildSortableItems(sanitized));
value: nextType,
};
});
setSortableItems(next.filter((item) => item.type));
}} }}
/> />
<FlatList <FlatList
@@ -17,9 +17,9 @@ const getScrollY = () => (typeof window !== "undefined" ? window.scrollY : 0);
const randomSuffix = () => Math.random().toString(36).slice(2, 8); const randomSuffix = () => Math.random().toString(36).slice(2, 8);
const buildItems = (structure = []) => { const buildSortableItems = (values = []) => {
const counters = {}; const counters = {};
return structure.map((segment, index) => { return sanitizeStructureList(values).map((segment, index) => {
counters[segment] = (counters[segment] || 0) + 1; counters[segment] = (counters[segment] || 0) + 1;
return { return {
id: `${segment}-${index}-${randomSuffix()}`, id: `${segment}-${index}-${randomSuffix()}`,
@@ -44,7 +44,7 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
() => sanitizeStructureList(baseStructure), () => sanitizeStructureList(baseStructure),
[baseStructure] [baseStructure]
); );
const [items, setItems] = React.useState(() => buildItems(sanitized)); const [items, setItems] = React.useState(() => buildSortableItems(sanitized));
const [activeItemId, setActiveItemId] = React.useState(null); const [activeItemId, setActiveItemId] = React.useState(null);
const containerRef = React.useRef(null); const containerRef = React.useRef(null);
const dragStateRef = React.useRef({ const dragStateRef = React.useRef({
@@ -54,38 +54,40 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
React.useEffect(() => { React.useEffect(() => {
console.debug("[CustomizeSongStructure.web] sanitized", sanitized); console.debug("[CustomizeSongStructure.web] sanitized", sanitized);
setItems((prev) => { setItems(buildSortableItems(sanitized));
const prevTypes = prev.map((item) => item.type);
if (prevTypes.length === sanitized.length) {
const same = prevTypes.every((type, idx) => type === sanitized[idx]);
if (same) return prev;
}
return buildItems(sanitized);
});
dragStateRef.current = { itemId: null, containerTop: 0 }; dragStateRef.current = { itemId: null, containerTop: 0 };
setActiveItemId(null); setActiveItemId(null);
}, [sanitized]); }, [sanitized]);
React.useEffect(() => { React.useEffect(() => {
console.debug("[CustomizeSongStructure.web] items", items); console.debug("[CustomizeSongStructure.web] items", items);
onChange?.(items.map((item) => item.type)); onChange?.(
items.map((item) => (item?.type || item?.value)).filter(Boolean)
);
}, [items, onChange]); }, [items, onChange]);
const labeledItems = React.useMemo(() => { const labeledItems = React.useMemo(() => {
const counts = {}; const counts = {};
return items.map((item) => { return items.map((item) => {
counts[item.type] = (counts[item.type] || 0) + 1; const type = item?.type || item?.value;
counts[type] = (counts[type] || 0) + 1;
return { return {
...item, ...item,
label: formatStructureLabel(item.type, counts[item.type]), type,
value: type,
label: formatStructureLabel(type, counts[type]),
}; };
}); });
}, [items]); }, [items]);
const selectedTypes = React.useMemo( const selectedTypes = React.useMemo(() => {
() => new Set(items.map((item) => item.type)), const set = new Set();
[items] items.forEach((item) => {
); if (item?.type) set.add(item.type);
else if (item?.value) set.add(item.value);
});
return set;
}, [items]);
const optionalSegments = React.useMemo( const optionalSegments = React.useMemo(
() => () =>
@@ -98,24 +100,21 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
const toggleOptionalSegment = React.useCallback((type) => { const toggleOptionalSegment = React.useCallback((type) => {
setItems((prev) => { setItems((prev) => {
const exists = prev.some((item) => item.type === type); const currentValues = prev.map((item) => item.type || item.value);
if (exists) { if (currentValues.includes(type)) {
return prev.filter((item) => item.type !== type); const filtered = currentValues.filter((value) => value !== type);
return buildSortableItems(filtered);
} }
const meta = getSegmentMeta(type); const meta = getSegmentMeta(type);
let next = [...prev]; let nextValues = [...currentValues];
if (meta?.exclusiveGroup) { if (meta?.exclusiveGroup) {
next = next.filter((item) => { nextValues = nextValues.filter((value) => {
const itemMeta = getSegmentMeta(item.type); const itemMeta = getSegmentMeta(value);
return itemMeta?.exclusiveGroup !== meta.exclusiveGroup; return itemMeta?.exclusiveGroup !== meta.exclusiveGroup;
}); });
} }
next.push({ nextValues.push(type);
id: `${type}-${Date.now()}-${randomSuffix()}`, return buildSortableItems(nextValues);
type,
value: type,
});
return next;
}); });
}, []); }, []);
@@ -169,6 +168,9 @@ const CustomizeSongStructure = ({ baseStructure = [], onChange }) => {
}; };
const handleRelease = () => { const handleRelease = () => {
setItems((prev) =>
buildSortableItems(prev.map((item) => item.type || item.value))
);
dragStateRef.current = { dragStateRef.current = {
itemId: null, itemId: null,
containerTop: 0, containerTop: 0,
+72 -14
View File
@@ -17,9 +17,11 @@ import { Palette, gutters } from "../../styles";
import { FONT_FAMILY } from "../../styles/Fonts"; import { FONT_FAMILY } from "../../styles/Fonts";
import { import {
formatStructureLabel, formatStructureLabel,
getPromptLabelForStructure,
getSegmentMeta, getSegmentMeta,
normalizeStructureType, normalizeStructureType,
sanitizeStructureList, sanitizeStructureList,
segmentRequiresLyrics,
} from "../../utils/songStructure"; } from "../../utils/songStructure";
import CustomInput from "./components/CustomInput"; import CustomInput from "./components/CustomInput";
@@ -52,21 +54,42 @@ const Lyrics = ({ navigation }) => {
? sanitizeStructureList(projectConfig.structure) ? sanitizeStructureList(projectConfig.structure)
: null; : null;
if (normalizedSections.length) { const structureToUse =
return { targetStructure && targetStructure.length
title, ? targetStructure
sections: normalizedSections, : normalizedSections.map((s) => s.type);
};
if (!structureToUse.length && normalizedSections.length) {
return { title, sections: normalizedSections };
} }
if (targetStructure && targetStructure.length) { const remaining = [...normalizedSections];
return { const takeMatching = (type) => {
title, const index = remaining.findIndex((s) => s.type === type);
sections: targetStructure.map((t) => ({ type: t, lyrics: "" })), if (index !== -1) {
}; return remaining.splice(index, 1)[0];
} }
return remaining.shift() || null;
};
return { title, sections: [] }; const sections = structureToUse.map((segmentType) => {
const normalizedType = normalizeStructureType(segmentType);
if (!segmentRequiresLyrics(normalizedType)) {
return { type: normalizedType, lyrics: "" };
}
const matched = takeMatching(normalizedType);
return {
type: normalizedType,
lyrics: matched?.lyrics || "",
};
});
const remainingTextual = remaining.filter((item) =>
segmentRequiresLyrics(item.type)
);
sections.push(...remainingTextual);
return { title, sections };
}, [projectTitle, projectLyrics, projectConfig]); }, [projectTitle, projectLyrics, projectConfig]);
const [titleValue, setTitleValue] = useState(initial.title || ""); const [titleValue, setTitleValue] = useState(initial.title || "");
@@ -131,7 +154,11 @@ const Lyrics = ({ navigation }) => {
alertMessage("Titre manquant", "Veuillez renseigner un titre."); alertMessage("Titre manquant", "Veuillez renseigner un titre.");
return; return;
} }
const invalid = (sections || []).some((s) => !(s?.lyrics || "").trim()); const invalid = (sections || []).some((s) => {
const type = normalizeStructureType(s?.type);
if (!segmentRequiresLyrics(type)) return false;
return !(s?.lyrics || "").trim();
});
if (invalid) { if (invalid) {
alertMessage( alertMessage(
"Champs incomplets", "Champs incomplets",
@@ -141,7 +168,9 @@ const Lyrics = ({ navigation }) => {
} }
const normalizedNewLyrics = (sections || []).map((s) => ({ const normalizedNewLyrics = (sections || []).map((s) => ({
type: normalizeStructureType(s?.type), type: normalizeStructureType(s?.type),
lyrics: (s?.lyrics || "").trim(), lyrics: segmentRequiresLyrics(normalizeStructureType(s?.type))
? (s?.lyrics || "").trim()
: "",
})); }));
const normalizedOldLyrics = Array.isArray(projectLyrics) const normalizedOldLyrics = Array.isArray(projectLyrics)
? projectLyrics.map((s) => ({ ? projectLyrics.map((s) => ({
@@ -349,6 +378,18 @@ const Lyrics = ({ navigation }) => {
? 170 ? 170
: 225; : 225;
const placeholder = meta?.label || label; const placeholder = meta?.label || label;
const requiresLyrics = segmentRequiresLyrics(key);
if (!requiresLyrics) {
return (
<View key={idx} style={styles.instrumentalBlock}>
<Text style={styles.instrumentalTitle}>{label}</Text>
<Text style={styles.instrumentalText}>
{getPromptLabelForStructure(key)} section instrumentale
sans paroles.
</Text>
</View>
);
}
return ( return (
<CustomInput <CustomInput
key={idx} key={idx}
@@ -417,4 +458,21 @@ const styles = StyleSheet.create({
instructionsHighlight: { instructionsHighlight: {
fontFamily: FONT_FAMILY.InterSemiBold, fontFamily: FONT_FAMILY.InterSemiBold,
}, },
instrumentalBlock: {
padding: 16,
borderRadius: 12,
backgroundColor: Palette.ultraLightWhite,
gap: 6,
},
instrumentalTitle: {
fontFamily: FONT_FAMILY.InterSemiBold,
fontSize: 16,
color: Palette.white,
},
instrumentalText: {
fontFamily: FONT_FAMILY.InterRegular,
fontSize: 14,
color: Palette.white,
opacity: 0.8,
},
}); });
+50 -1
View File
@@ -20,6 +20,7 @@ export const STRUCTURE_SEGMENTS = {
allowMultiple: true, allowMultiple: true,
optional: false, optional: false,
inputHeight: 225, inputHeight: 225,
requiresLyrics: true,
aliases: ["vers", "verse", "couplet"], aliases: ["vers", "verse", "couplet"],
}, },
refrain: { refrain: {
@@ -29,6 +30,7 @@ export const STRUCTURE_SEGMENTS = {
allowMultiple: true, allowMultiple: true,
optional: false, optional: false,
inputHeight: 170, inputHeight: 170,
requiresLyrics: true,
aliases: ["chorus", "refrain"], aliases: ["chorus", "refrain"],
}, },
short_intro: { short_intro: {
@@ -39,6 +41,7 @@ export const STRUCTURE_SEGMENTS = {
optional: true, optional: true,
inputHeight: 150, inputHeight: 150,
exclusiveGroup: "intro", exclusiveGroup: "intro",
requiresLyrics: false,
aliases: [ aliases: [
"introduction instrumentale courte", "introduction instrumentale courte",
"intro courte", "intro courte",
@@ -54,6 +57,7 @@ export const STRUCTURE_SEGMENTS = {
optional: true, optional: true,
inputHeight: 150, inputHeight: 150,
exclusiveGroup: "intro", exclusiveGroup: "intro",
requiresLyrics: false,
aliases: [ aliases: [
"introduction instrumentale longue", "introduction instrumentale longue",
"intro longue", "intro longue",
@@ -68,6 +72,7 @@ export const STRUCTURE_SEGMENTS = {
allowMultiple: false, allowMultiple: false,
optional: true, optional: true,
inputHeight: 150, inputHeight: 150,
requiresLyrics: true,
aliases: [ aliases: [
"pré-refrain", "pré-refrain",
"pre-refrain", "pre-refrain",
@@ -101,6 +106,12 @@ export const getSegmentMeta = (type) => {
return STRUCTURE_SEGMENTS[key] || null; 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( export const OPTIONAL_STRUCTURE_SEGMENTS = Object.keys(
STRUCTURE_SEGMENTS STRUCTURE_SEGMENTS
).filter((key) => STRUCTURE_SEGMENTS[key]?.optional); ).filter((key) => STRUCTURE_SEGMENTS[key]?.optional);
@@ -121,6 +132,7 @@ export const normalizeStructureType = (value) => {
export const sanitizeStructureList = (structure = []) => { export const sanitizeStructureList = (structure = []) => {
if (!Array.isArray(structure)) return []; if (!Array.isArray(structure)) return [];
const output = []; const output = [];
let shouldInjectPreChorus = false;
structure.forEach((segment) => { structure.forEach((segment) => {
const type = normalizeStructureType(segment); const type = normalizeStructureType(segment);
@@ -144,10 +156,47 @@ export const sanitizeStructureList = (structure = []) => {
} }
} }
if (type === "pre_chorus") {
shouldInjectPreChorus = true;
return;
}
output.push(type); output.push(type);
}); });
return output; let result = output;
if (shouldInjectPreChorus) {
const baseWithoutPre = result.filter((item) => item !== "pre_chorus");
const hasRefrain = baseWithoutPre.some((item) => item === "refrain");
const expanded = [];
baseWithoutPre.forEach((item, idx) => {
if (item === "refrain") {
const previous = expanded[expanded.length - 1];
if (previous !== "pre_chorus") {
expanded.push("pre_chorus");
}
}
expanded.push(item);
});
if (!hasRefrain) {
expanded.push("pre_chorus");
}
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) => { export const formatStructureLabel = (type, occurrence = 1) => {