parent
0a611a4f59
commit
0a50def3c8
@ -0,0 +1,67 @@
|
||||
CREATE TYPE "public"."formacion_vocal" AS ENUM('hombre', 'mujer', 'dueto-femenino', 'dueto-masculino', 'dueto-mixto', 'multiple-mixto', 'multiple-femenino', 'multiple-masculino');--> statement-breakpoint
|
||||
DROP VIEW "cancion_principal";--> statement-breakpoint
|
||||
ALTER TABLE "cancion" RENAME COLUMN "instrumental" TO "tiene_letra";--> statement-breakpoint
|
||||
-- La columna anterior expresaba el concepto al revés: instrumental=true
|
||||
-- significaba que no había letra. El renombrado conserva físicamente el dato;
|
||||
-- esta inversión conserva su significado.
|
||||
ALTER TABLE "cancion" ALTER COLUMN "tiene_letra" DROP DEFAULT;--> statement-breakpoint
|
||||
UPDATE "cancion" SET "tiene_letra" = NOT "tiene_letra";--> statement-breakpoint
|
||||
ALTER TABLE "cancion" ALTER COLUMN "tiene_letra" SET DEFAULT true;--> statement-breakpoint
|
||||
ALTER TABLE "cancion" ALTER COLUMN "idioma" DROP DEFAULT;--> statement-breakpoint
|
||||
ALTER TABLE "cancion" ADD COLUMN "formacion_vocal" "formacion_vocal";--> statement-breakpoint
|
||||
-- Recupera únicamente las voces que el prompt nombra de forma explícita. Si
|
||||
-- no aparece una pista inequívoca, el campo permanece nulo para que el panel
|
||||
-- muestre «Sin indicar» en vez de fabricar un dato.
|
||||
WITH estilos AS (
|
||||
SELECT DISTINCT ON (p."cancion_id")
|
||||
p."cancion_id",
|
||||
lower(p."contenido" ->> 'estilo') AS texto
|
||||
FROM "prompt" p
|
||||
WHERE p."contenido" ->> 'estilo' IS NOT NULL
|
||||
ORDER BY p."cancion_id", p."preferido" DESC, p."creado_en" DESC
|
||||
), detectadas AS (
|
||||
SELECT "cancion_id",
|
||||
CASE
|
||||
WHEN texto ~ '(^|[^a-z])(choir|vocal ensemble|ensemble vocal|multiple vocals?|multiple voices|coro|coral)([^a-z]|$)'
|
||||
AND texto ~ '(^|[^a-z])(female|woman|women|feminine|femme|mujer|mujeres)([^a-z]|$)'
|
||||
AND texto ~ '(^|[^a-z])(male|man|men|masculine|homme|hombre|hombres|baritone|tenor|crooner)([^a-z]|$)'
|
||||
THEN 'multiple-mixto'
|
||||
WHEN texto ~ '(^|[^a-z])(choir|vocal ensemble|ensemble vocal|multiple vocals?|multiple voices|coro|coral)([^a-z]|$)'
|
||||
AND texto ~ '(^|[^a-z])(female|woman|women|feminine|femme|mujer|mujeres)([^a-z]|$)'
|
||||
THEN 'multiple-femenino'
|
||||
WHEN texto ~ '(^|[^a-z])(choir|vocal ensemble|ensemble vocal|multiple vocals?|multiple voices|coro|coral)([^a-z]|$)'
|
||||
AND texto ~ '(^|[^a-z])(male|man|men|masculine|homme|hombre|hombres|baritone|tenor|crooner)([^a-z]|$)'
|
||||
THEN 'multiple-masculino'
|
||||
WHEN texto ~ '(homme/femme|femme/homme|male/female|female/male)'
|
||||
OR (
|
||||
texto ~ '(^|[^a-z])(duet|duo|dueto|double vocals?|doble voz)([^a-z]|$)'
|
||||
AND texto ~ '(^|[^a-z])(female|woman|women|feminine|femme|mujer|mujeres)([^a-z]|$)'
|
||||
AND texto ~ '(^|[^a-z])(male|man|men|masculine|homme|hombre|hombres|baritone|tenor|crooner)([^a-z]|$)'
|
||||
)
|
||||
THEN 'dueto-mixto'
|
||||
WHEN texto ~ '(^|[^a-z])(duet|duo|dueto|double vocals?|doble voz)([^a-z]|$)'
|
||||
AND texto ~ '(^|[^a-z])(female|woman|women|feminine|femme|mujer|mujeres)([^a-z]|$)'
|
||||
THEN 'dueto-femenino'
|
||||
WHEN texto ~ '(^|[^a-z])(duet|duo|dueto|double vocals?|doble voz)([^a-z]|$)'
|
||||
AND texto ~ '(^|[^a-z])(male|man|men|masculine|homme|hombre|hombres|baritone|tenor|crooner)([^a-z]|$)'
|
||||
THEN 'dueto-masculino'
|
||||
WHEN texto ~ '(^|[^a-z])(female|woman|women|feminine|femme|mujer|mujeres)([^a-z]|$)'
|
||||
THEN 'mujer'
|
||||
WHEN texto ~ '(^|[^a-z])(male|man|men|masculine|homme|hombre|hombres|baritone|tenor|crooner)([^a-z]|$)'
|
||||
THEN 'hombre'
|
||||
END AS formacion
|
||||
FROM estilos
|
||||
)
|
||||
UPDATE "cancion" c
|
||||
SET "formacion_vocal" = d.formacion::formacion_vocal
|
||||
FROM detectadas d
|
||||
WHERE d."cancion_id" = c."id" AND d.formacion IS NOT NULL;--> statement-breakpoint
|
||||
-- Una versión nueva hereda estos datos; las ya existentes hacen lo mismo una
|
||||
-- sola vez durante la migración y después pueden editarse por separado.
|
||||
UPDATE "cancion" v
|
||||
SET "formacion_vocal" = p."formacion_vocal"
|
||||
FROM "cancion" p
|
||||
WHERE v."version_de_id" = p."id" AND v."formacion_vocal" IS NULL;--> statement-breakpoint
|
||||
ALTER TABLE "cancion" ADD CONSTRAINT "cancion_idioma_bcp47" CHECK ("cancion"."idioma" ~ '^[a-z]{2,8}(-[A-Za-z0-9]{1,8})*$');--> statement-breakpoint
|
||||
CREATE VIEW "cancion_principal" AS
|
||||
SELECT * FROM "cancion" WHERE "version_de_id" IS NULL;
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,23 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { formacionVocalDe } from './carpeta.mjs';
|
||||
|
||||
describe('formacionVocalDe', () => {
|
||||
it.each([
|
||||
['powerful female vocal', 'mujer'],
|
||||
['warm baritone vocals', 'hombre'],
|
||||
['Voix Homme/Femme, double vocals', 'dueto-mixto'],
|
||||
['mixed duet with piano', 'dueto-mixto'],
|
||||
['female duet with piano', 'dueto-femenino'],
|
||||
['male duet with acoustic guitars', 'dueto-masculino'],
|
||||
['female vocal ensemble', 'multiple-femenino'],
|
||||
['male vocal ensemble', 'multiple-masculino'],
|
||||
['mixed choir', 'multiple-mixto']
|
||||
])('reconoce %s', (prompt, formacion) => {
|
||||
expect(formacionVocalDe(prompt)).toBe(formacion);
|
||||
});
|
||||
|
||||
it('no inventa una voz cuando el prompt no la declara', () => {
|
||||
expect(formacionVocalDe('pop rock, alternative rock, atmospheric')).toBeNull();
|
||||
expect(formacionVocalDe(null)).toBeNull();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,33 @@
|
||||
/**
|
||||
* Composición de las voces que cantan una grabación.
|
||||
*
|
||||
* No sustituye a los intérpretes: estos valores describen la formación vocal;
|
||||
* `cancion_interprete` conserva las identidades concretas y sus créditos.
|
||||
*/
|
||||
export const FORMACIONES_VOCALES = [
|
||||
'hombre',
|
||||
'mujer',
|
||||
'dueto-femenino',
|
||||
'dueto-masculino',
|
||||
'dueto-mixto',
|
||||
'multiple-mixto',
|
||||
'multiple-femenino',
|
||||
'multiple-masculino'
|
||||
] as const;
|
||||
|
||||
export type FormacionVocal = (typeof FORMACIONES_VOCALES)[number];
|
||||
|
||||
export const NOMBRES_FORMACION_VOCAL: Record<FormacionVocal, string> = {
|
||||
hombre: 'Hombre',
|
||||
mujer: 'Mujer',
|
||||
'dueto-femenino': 'Dueto femenino',
|
||||
'dueto-masculino': 'Dueto masculino',
|
||||
'dueto-mixto': 'Dueto mixto',
|
||||
'multiple-mixto': 'Múltiple mixto',
|
||||
'multiple-femenino': 'Múltiple femenino',
|
||||
'multiple-masculino': 'Múltiple masculino'
|
||||
};
|
||||
|
||||
export function esFormacionVocal(valor: string): valor is FormacionVocal {
|
||||
return FORMACIONES_VOCALES.includes(valor as FormacionVocal);
|
||||
}
|
||||
@ -0,0 +1,18 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { nombreDeIdioma, normalizarIdioma } from './idiomas';
|
||||
|
||||
describe('idiomas de las canciones', () => {
|
||||
it('normaliza etiquetas BCP 47 sin reducirlas a dos letras', () => {
|
||||
expect(normalizarIdioma(' pt_br ')).toBe('pt-BR');
|
||||
expect(normalizarIdioma('zh-hant')).toBe('zh-Hant');
|
||||
});
|
||||
|
||||
it('rechaza nombres libres y códigos inexistentes', () => {
|
||||
expect(normalizarIdioma('español')).toBeUndefined();
|
||||
expect(normalizarIdioma('')).toBeUndefined();
|
||||
});
|
||||
|
||||
it('presenta el nombre sin una tabla duplicada', () => {
|
||||
expect(nombreDeIdioma('fr')).toMatch(/francés/i);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,32 @@
|
||||
/**
|
||||
* Idiomas de las canciones.
|
||||
*
|
||||
* Se guardan como etiquetas BCP 47 porque es el mismo formato que entiende el
|
||||
* atributo HTML `lang`: `es`, `fr`, `en`, pero también `pt-BR` o `zh-Hant`.
|
||||
* No hay una lista cerrada en código; añadir un idioma no debería exigir un
|
||||
* despliegue.
|
||||
*/
|
||||
|
||||
/** Devuelve la etiqueta canónica, o `undefined` si no es un código BCP 47. */
|
||||
export function normalizarIdioma(valor: string): string | undefined {
|
||||
const limpio = valor.trim().replaceAll('_', '-');
|
||||
if (!limpio) return undefined;
|
||||
|
||||
try {
|
||||
return Intl.getCanonicalLocales(limpio)[0];
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
/** Nombre localizado sin mantener una tabla de idiomas propia. */
|
||||
export function nombreDeIdioma(codigo: string, locale = 'es'): string {
|
||||
const normalizado = normalizarIdioma(codigo);
|
||||
if (!normalizado) return codigo;
|
||||
|
||||
try {
|
||||
return new Intl.DisplayNames([locale], { type: 'language' }).of(normalizado) ?? normalizado;
|
||||
} catch {
|
||||
return normalizado;
|
||||
}
|
||||
}
|
||||
Loading…
Reference in new issue