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
+50 -9
View File
@@ -1,7 +1,7 @@
import type { StillGenerationSettings } from '~/utils/libraryGeneration'
import type { LoraStackItem } from '~/utils/loras'
export type LibraryFilter = 'all' | 'images' | 'videos'
export type LibraryFilter = 'all' | 'images' | 'videos' | 'music'
export type LibrarySort = 'date-desc' | 'date-asc' | 'name-asc' | 'name-desc' | 'type'
export type LibraryFolder = {
@@ -13,6 +13,7 @@ export type LibraryFolder = {
unlocked: boolean
clipCount?: number
imageCount?: number
trackCount?: number
}
export type LibraryStill = {
@@ -66,15 +67,37 @@ export type LibraryClip = {
loraStack?: LoraStackItem[]
}
export type LibraryTrack = {
id: string
folderId: string
name: string
tags: string
lyrics: string
duration: number
steps: number
seed: number
cfg: number
instrumental: boolean
createdAt: number
filename: string
ext: string
engine?: string
samplerName?: string
scheduler?: string
lyricsStrength?: number
hasAudio?: boolean
}
export type ClipGroup = { key: string; head: LibraryClip; parts: LibraryClip[] }
export type StillGroup = { key: string; head: LibraryStill; parts: LibraryStill[] }
export type LibraryEntry = {
kind: 'clip' | 'still'
kind: 'clip' | 'still' | 'track'
key: string
createdAt: number
name: string
group?: ClipGroup
stillGroup?: StillGroup
track?: LibraryTrack
}
export function familyBaseName(name: string) {
@@ -248,6 +271,7 @@ export function stillCollectionTitle(group: StillGroup) {
export function entryTitle(entry: LibraryEntry) {
if (entry.kind === 'clip' && entry.group) return collectionTitle(entry.group)
if (entry.kind === 'track' && entry.track) return entry.track.name
if (entry.stillGroup) return stillCollectionTitle(entry.stillGroup)
return entry.name
}
@@ -256,6 +280,9 @@ export function entrySearchText(entry: LibraryEntry) {
if (entry.kind === 'clip' && entry.group) {
return entry.group.parts.map(part => `${part.name || ''} ${part.prompt || ''}`).join(' ')
}
if (entry.kind === 'track' && entry.track) {
return `${entry.track.name || ''} ${entry.track.tags || ''} ${entry.track.lyrics || ''}`
}
if (entry.stillGroup) {
return entry.stillGroup.parts.map(part => `${part.name || ''} ${part.prompt || ''} ${part.filename || ''}`).join(' ')
}
@@ -297,6 +324,7 @@ function compareName(a: string, b: string) {
export function buildLibraryEntries(opts: {
clips: LibraryClip[]
stills: LibraryStill[]
tracks?: LibraryTrack[]
folderId: string
filter: LibraryFilter
search: string
@@ -305,7 +333,8 @@ export function buildLibraryEntries(opts: {
const inFolder = (folderId: string) => !opts.folderId || opts.folderId === 'all' || folderId === opts.folderId
const clips = opts.clips.filter(clip => inFolder(clip.folderId) && clip.hasVideo !== false)
const stills = opts.stills.filter(still => inFolder(still.folderId) && still.role === 'output')
const videos: LibraryEntry[] = opts.filter === 'images'
const tracks = (opts.tracks || []).filter(track => inFolder(track.folderId) && track.hasAudio !== false)
const videos: LibraryEntry[] = opts.filter === 'images' || opts.filter === 'music'
? []
: groupClips(clips).map(group => ({
kind: 'clip' as const,
@@ -314,7 +343,7 @@ export function buildLibraryEntries(opts: {
name: collectionTitle(group),
group
}))
const images: LibraryEntry[] = opts.filter === 'videos'
const images: LibraryEntry[] = opts.filter === 'videos' || opts.filter === 'music'
? []
: groupStills(stills).map(group => ({
kind: 'still' as const,
@@ -323,7 +352,16 @@ export function buildLibraryEntries(opts: {
name: stillCollectionTitle(group),
stillGroup: group
}))
let entries = [...videos, ...images]
const music: LibraryEntry[] = opts.filter === 'images' || opts.filter === 'videos'
? []
: tracks.map(track => ({
kind: 'track' as const,
key: `track:${track.id}`,
createdAt: track.createdAt,
name: track.name,
track
}))
let entries = [...videos, ...images, ...music]
const needle = opts.search.trim().toLowerCase()
if (needle) {
entries = entries.filter(entry => entrySearchText(entry).toLowerCase().includes(needle))
@@ -334,7 +372,8 @@ export function buildLibraryEntries(opts: {
if (sort === 'name-desc') return compareName(b.name, a.name) || b.createdAt - a.createdAt
if (sort === 'date-asc') return a.createdAt - b.createdAt
if (sort === 'type') {
if (a.kind !== b.kind) return a.kind === 'still' ? -1 : 1
const order = { still: 0, clip: 1, track: 2 } as const
if (a.kind !== b.kind) return order[a.kind] - order[b.kind]
return b.createdAt - a.createdAt
}
return b.createdAt - a.createdAt
@@ -342,13 +381,15 @@ export function buildLibraryEntries(opts: {
return entries
}
export function folderOutputCount(folder: LibraryFolder, clips: LibraryClip[], stills: LibraryStill[], filter: LibraryFilter) {
if (!folder.unlocked) return (folder.clipCount || 0) + (folder.imageCount || 0)
export function folderOutputCount(folder: LibraryFolder, clips: LibraryClip[], stills: LibraryStill[], filter: LibraryFilter, tracks: LibraryTrack[] = []) {
if (!folder.unlocked) return (folder.clipCount || 0) + (folder.imageCount || 0) + (folder.trackCount || 0)
const videos = clips.filter(clip => clip.folderId === folder.id).length
const images = stills.filter(still => still.folderId === folder.id && still.role === 'output').length
const music = tracks.filter(track => track.folderId === folder.id).length
if (filter === 'videos') return videos
if (filter === 'images') return images
return videos + images
if (filter === 'music') return music
return videos + images + music
}
export function formatWhen(createdAt: number) {