|
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; |
|
} |
|
|
|
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', |
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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({ |
|
|
|
version: '7a76a8258b23fae65c5a22debb8841d1d7e816b75c2f24218cd2bd8573787906', |
|
input: { |
|
model_version, |
|
prompt, |
|
duration, |
|
normalization_strategy, |
|
top_k, |
|
top_p, |
|
temperature, |
|
classifer_free_gudance, |
|
output_format, |
|
seed, |
|
}, |
|
webhook, |
|
webhook_events_filter, |
|
}); |
|
} |
|
|