Files
aigen/utils/music.ts
T

133 lines
4.6 KiB
TypeScript

export const MUSIC_DURATION_MIN = 30
export const MUSIC_DURATION_MAX = 180
export const MUSIC_DURATION_DEFAULT = 60
export const MUSIC_STEPS_MIN = 8
export const MUSIC_STEPS_MAX = 100
export const MUSIC_STEPS_DEFAULT = 50
export const MUSIC_STEPS_DEFAULT_15 = 8
export const MUSIC_CFG_DEFAULT = 5
export const MUSIC_CFG_DEFAULT_15 = 1
export const MUSIC_LYRICS_STRENGTH_DEFAULT = 0.9
export const MUSIC_TAGS_MAX = 2000
export const MUSIC_LYRICS_MAX = 8000
export const MUSIC_STRUCTURE_CHIPS = [
'[Verse 1]',
'[Verse 2]',
'[Pre-Chorus]',
'[Chorus]',
'[Bridge]',
'[Guitar Solo]',
'[Instrumental]',
'[Outro]'
] as const
export const DEFAULT_MUSIC_TAGS = 'modern synthpop, 120 bpm, female vocals, punchy bass, analog synths'
export const DEFAULT_MUSIC_LYRICS = `[Verse 1]
[Chorus]
[Guitar Solo]
[Outro]
`
export type MusicEngine = 'ace-step' | 'ace-step-1.5' | 'yue'
export function parseMusicEngine(raw: unknown): MusicEngine {
const value = String(raw || '').trim().toLowerCase()
if (value === 'ace-step-1.5' || value === 'ace15' || value === 'v1.5') return 'ace-step-1.5'
if (value === 'yue' || value === 'yue-s1' || value === 'lyrics2song') return 'yue'
return 'ace-step'
}
export function isAceMusicEngine(engine: MusicEngine | string | undefined) {
return parseMusicEngine(engine) !== 'yue'
}
export function musicEngineLabel(engine: MusicEngine | string | undefined) {
const value = parseMusicEngine(engine)
if (value === 'ace-step-1.5') return 'ACE-Step 1.5'
if (value === 'yue') return 'YuE'
return 'ACE-Step'
}
export function parseMusicBpm(tags: unknown, fallback = 120) {
const match = String(tags || '').match(/(\d{2,3})\s*bpm/i)
if (!match) return fallback
const value = Number(match[1])
if (!Number.isFinite(value)) return fallback
return Math.min(300, Math.max(10, Math.round(value)))
}
export function clampMusicDuration(raw: unknown, fallback = MUSIC_DURATION_DEFAULT) {
const value = Number(raw)
if (!Number.isFinite(value)) return fallback
return Math.min(MUSIC_DURATION_MAX, Math.max(MUSIC_DURATION_MIN, Math.round(value)))
}
export function clampMusicSteps(raw: unknown, fallback = MUSIC_STEPS_DEFAULT) {
const value = Number(raw)
if (!Number.isFinite(value)) return fallback
return Math.min(MUSIC_STEPS_MAX, Math.max(MUSIC_STEPS_MIN, Math.round(value)))
}
export function clampMusicCfg(raw: unknown, fallback = MUSIC_CFG_DEFAULT) {
const value = Number(raw)
if (!Number.isFinite(value)) return fallback
return Math.min(15, Math.max(1, Math.round(value * 10) / 10))
}
export function clampLyricsStrength(raw: unknown, fallback = MUSIC_LYRICS_STRENGTH_DEFAULT) {
const value = Number(raw)
if (!Number.isFinite(value)) return fallback
return Math.min(2, Math.max(0, Math.round(value * 100) / 100))
}
export function sanitizeMusicTags(raw: unknown) {
return String(raw || '').replace(/\r\n/g, '\n').trim().slice(0, MUSIC_TAGS_MAX)
}
export function sanitizeMusicLyrics(raw: unknown, instrumental = false) {
if (instrumental) return ''
return String(raw || '').replace(/\r\n/g, '\n').trim().slice(0, MUSIC_LYRICS_MAX)
}
/** YuE splits on [Section] headers. Returns how many sections are present. */
export function countYueLyricSections(lyrics: unknown) {
const text = String(lyrics || '')
const matches = text.match(/\[[^\]]+\]/g)
return matches ? matches.length : 0
}
export function yueLyricsProblem(lyrics: unknown): string | null {
const text = String(lyrics || '').trim()
if (!text) return 'YuE needs lyrics with section tags like [Verse 1] and [Chorus].'
const sections = [...text.matchAll(/\[([^\]]+)\]\s*([^\[]*)/gs)].filter(match => match[2].trim())
if (sections.length !== 1) return 'YuEGP needs exactly one non-empty lyric section. Combine the lyrics under one heading.'
return null
}
export function parseMusicSeed(raw: unknown) {
const text = String(raw ?? '').trim().toLowerCase()
if (!text || text === 'random') return Math.floor(Math.random() * 2_147_483_647)
const value = Number(text)
if (!Number.isFinite(value)) return Math.floor(Math.random() * 2_147_483_647)
return Math.max(0, Math.min(2_147_483_647, Math.floor(value)))
}
export function musicContentType(ext: string) {
const kind = String(ext || '').replace(/^\./, '').toLowerCase()
if (kind === 'wav') return 'audio/wav'
if (kind === 'flac') return 'audio/flac'
if (kind === 'mp3') return 'audio/mpeg'
if (kind === 'ogg' || kind === 'opus') return 'audio/ogg'
return 'application/octet-stream'
}
export function musicExtension(filename: string) {
const match = String(filename || '').toLowerCase().match(/\.(wav|flac|mp3|ogg|opus)$/)
return match ? match[1] : 'flac'
}