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_CFG_DEFAULT = 5 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' 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' return 'ace-step' } 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) } 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' }