Add ACE-Step music generation to the studio and library.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Towsty
2026-09-02 23:58:18 -05:00
co-authored by Cursor
parent c79cca39ae
commit 5555155884
25 changed files with 2157 additions and 44 deletions
+95
View File
@@ -0,0 +1,95 @@
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'
}