Spaces:
Sleeping
Sleeping
import { v } from 'convex/values'; | |
import { query, internalMutation } from './_generated/server'; | |
import Replicate, { WebhookEventType } from 'replicate'; | |
import { httpAction, internalAction } from './_generated/server'; | |
import { internal, api } from './_generated/api'; | |
function client(): Replicate { | |
const replicate = new Replicate({ | |
auth: process.env.REPLICATE_API_TOKEN || '', | |
}); | |
return replicate; | |
} | |
function replicateAvailable(): boolean { | |
return !!process.env.REPLICATE_API_TOKEN; | |
} | |
export const insertMusic = internalMutation({ | |
args: { storageId: v.string(), type: v.union(v.literal('background'), v.literal('player')) }, | |
handler: async (ctx, args) => { | |
await ctx.db.insert('music', { | |
storageId: args.storageId, | |
type: args.type, | |
}); | |
}, | |
}); | |
export const getBackgroundMusic = query({ | |
handler: async (ctx) => { | |
const music = await ctx.db | |
.query('music') | |
.filter((entry) => entry.eq(entry.field('type'), 'background')) | |
.order('desc') | |
.first(); | |
if (!music) { | |
return '/assets/background.mp3'; | |
} | |
const url = await ctx.storage.getUrl(music.storageId); | |
if (!url) { | |
throw new Error(`Invalid storage ID: ${music.storageId}`); | |
} | |
return url; | |
}, | |
}); | |
export const enqueueBackgroundMusicGeneration = internalAction({ | |
handler: async (ctx): Promise<void> => { | |
if (!replicateAvailable()) { | |
return; | |
} | |
const worldStatus = await ctx.runQuery(api.world.defaultWorldStatus); | |
if (!worldStatus) { | |
console.log('No active default world, returning.'); | |
return; | |
} | |
// TODO: MusicGen-Large on Replicate only allows 30 seconds. Use MusicGen-Small for longer? | |
await generateMusic('16-bit RPG adventure game with wholesome vibe', 30); | |
}, | |
}); | |
export const handleReplicateWebhook = httpAction(async (ctx, request) => { | |
const req = await request.json(); | |
if (req.id) { | |
const prediction = await client().predictions.get(req.id); | |
const response = await fetch(prediction.output); | |
const music = await response.blob(); | |
const storageId = await ctx.storage.store(music); | |
await ctx.runMutation(internal.music.insertMusic, { type: 'background', storageId }); | |
} | |
return new Response(); | |
}); | |
enum MusicGenNormStrategy { | |
Clip = 'clip', | |
Loudness = 'loudness', | |
Peak = 'peak', | |
Rms = 'rms', | |
} | |
enum MusicGenFormat { | |
wav = 'wav', | |
mp3 = 'mp3', | |
} | |
/** | |
* | |
* @param prompt A description of the music you want to generate. | |
* @param duration Duration of the generated audio in seconds. | |
* @param webhook webhook URL for Replicate to call when @param webhook_events_filter is triggered | |
* @param webhook_events_filter Array of event names to filter the webhook. See https://replicate.com/docs/reference/http#predictions.create--webhook_events_filter | |
* @param normalization_strategy Strategy for normalizing audio. | |
* @param top_k Reduces sampling to the k most likely tokens. | |
* @param top_p Reduces sampling to tokens with cumulative probability of p. When set to `0` (default), top_k sampling is used. | |
* @param temperature Controls the 'conservativeness' of the sampling process. Higher temperature means more diversity. | |
* @param classifer_free_gudance Increases the influence of inputs on the output. Higher values produce lower-varience outputs that adhere more closely to inputs. | |
* @param output_format Output format for generated audio. See @ | |
* @param seed Seed for random number generator. If None or -1, a random seed will be used. | |
* @returns object containing metadata of the prediction with ID to fetch once result is completed | |
*/ | |
export async function generateMusic( | |
prompt: string, | |
duration: number, | |
webhook: string = process.env.CONVEX_SITE_URL + '/replicate_webhook' || '', | |
webhook_events_filter: [WebhookEventType] = ['completed'], | |
normalization_strategy: MusicGenNormStrategy = MusicGenNormStrategy.Peak, | |
output_format: MusicGenFormat = MusicGenFormat.mp3, | |
top_k = 250, | |
top_p = 0, | |
temperature = 1, | |
classifer_free_gudance = 3, | |
seed = -1, | |
model_version = 'large', | |
) { | |
if (!replicateAvailable()) { | |
throw new Error('Replicate API token not set'); | |
} | |
return await client().predictions.create({ | |
// https://replicate.com/facebookresearch/musicgen/versions/7a76a8258b23fae65c5a22debb8841d1d7e816b75c2f24218cd2bd8573787906 | |
version: '7a76a8258b23fae65c5a22debb8841d1d7e816b75c2f24218cd2bd8573787906', | |
input: { | |
model_version, | |
prompt, | |
duration, | |
normalization_strategy, | |
top_k, | |
top_p, | |
temperature, | |
classifer_free_gudance, | |
output_format, | |
seed, | |
}, | |
webhook, | |
webhook_events_filter, | |
}); | |
} | |