用 Gemini 和 Lyria 构建 AI DJ
展示如何整合 Google Gemini 和音乐生成模型构建虚拟 DJ 应用,是多模型集成的实用工程案例
展示如何整合 Google Gemini 和音乐生成模型构建虚拟 DJ 应用,是多模型集成的实用工程案例
我最近做了一个实验,说实话,我已经听得停不下来了。它是一个基于 Web 的虚拟 Boombox。转动调谐旋钮,电台随之切换,音乐也会实时交叉淡化。
但真正的亮点是:DJ 是 AI。
每当你选定一个电台,动态生成的声音——由 Gemini text-to-speech 提供支持——便会适时响起,介绍当前曲目和音乐流派,而且完全理解当下的语境。它就像机器里的幽灵。整个项目使用 Google Gen AI SDK、Lit 和 Lyria Real-Time 模型构建。
下面是我的实现过程。
Vibe-coding 平台:Google AI Studio
框架:Lit(Web Components)+ Vite
音乐生成:Google 的 lyria-realtime-exp 模型
DJ 声音与台词:Gemini 2.5 Flash 和 Gemini TTS(Fenrir voice)
视觉效果:收音机使用 CSS 实现,背景由 Gemini 2.5 Flash Image 生成。
应用的核心是 LiveMusicHelper。它会连接 lyria-realtime-exp 模型。这里并不是生成一个静态 MP3 文件,而是建立一个会话,让我们可以通过发送 “Weighted Prompts” 实时引导音乐的走向。
当你转动 UI 上的调谐旋钮时,我们并不会下载一首新歌,而是在告诉 AI 转移它的关注方向。
// from utils/LiveMusicHelper.ts
public readonly setWeightedPrompts = throttle(async (prompts: Map<string, Prompt>) => {
// Convert our UI map to an array for the API
const weightedPrompts = this.activePrompts.map((p) => {
return { text: p.text, weight: p.weight };
});
try {
// This is where the magic happens.
// We tell the model: "be 100% Bossa Nova" or "mix 50% Dubstep and 50% Jazz"
await this.session.setWeightedPrompts({
weightedPrompts,
});
} catch (e: any) {
console.error(e);
}
}, 200);
可视化旋钮组件会把旋转角度映射为 prompt 数组中的索引。如果当前位于索引 0,Bossa Nova 就会获得 1.0 的权重。
这是我最喜欢的部分。如果没有 DJ 告诉你正在收听什么,收音机就不能算真正的收音机。为此,我创建了一个 RadioAnnouncer 类。
它通过一个两步调用链工作:
我们通过 prompt 让 Gemini 2.5 Flash 扮演一个特定角色。注意其中明确的约束:简短、有力、不使用引号。
// from utils/RadioAnnouncer.ts
const scriptResponse = await this.ai.models.generateContent({
model: 'gemini-2.5-flash',
contents: `You are a charismatic radio DJ. Write a single, short, punchy sentence to introduce the current song.
The station frequency is ${freq} FM.
The music genre is ${station}.
Do not use quotes. Just the spoken text.
Example: "You're locked in to 104.5, keeping it smooth with Bossa Nova."`,
});
我们使用预构建配置中的 Fenrir voice。
const ttsResponse = await this.ai.models.generateContent({
model: 'gemini-2.5-flash-preview-tts',
contents: {
parts: [{ text: script }]
},
config: {
responseModalities: [Modality.AUDIO],
speechConfig: {
voiceConfig: {
prebuiltVoiceConfig: { voiceName: 'Fenrir' }
}
}
}
});
用户可能会快速划过 5 个电台,只为了找到 Dubstep。我们并不希望 DJ 尝试播报经过的每一个电台。因此,我使用了 debouncer,只有当用户停止转动旋钮 800ms 后,生成流程才会触发。
UI 使用 Lit 构建。Boombox 本身主要通过 CSS 样式实现,扬声器和旋钮则混合使用了 SVG。
我使用 Web Audio API 创建了一个 AudioAnalyser。我们获取当前的频率数据,并将其映射到扬声器锥盆上的 CSS transform: scale()。
/* from components/PromptDjMidi.ts */
.speaker-cone {
/* ... textures and gradients ... */
transition: transform 0.05s cubic-bezier(0.1, 0.7, 1.0, 0.1);
}
// In the render loop
const pulseScale = 1 + (this.audioLevel * 0.15);
const speakerStyle = styleMap({
transform: `scale(${pulseScale})`
});
为了让整体氛围更有说服力,我会在加载时生成一张背景图。
// The prompt used for the background
text: 'A 90s-style girl\'s bedroom, dreamy, nostalgic, vaporwave aesthetic, anime posters on the wall, lava lamp, beaded curtains, photorealistic.'
这个项目最棘手的部分之一,是调谐旋钮背后的数学计算。我们需要把鼠标或触摸操作转换成旋转角度,再让旋转结果吸附到指定的“电台”。
我实现了一套环形捕获逻辑:
// from components/PromptDjMidi.ts
private handlePointerMove(e: PointerEvent) {
// ... math to get angle ...
// Handle crossing the 0/360 boundary smoothly
if (delta > 180) delta -= 360;
if (delta < -180) delta += 360;
this.rotation = (this.startRotation + delta + 360) % 360;
// Map rotation to station index
const index = Math.floor(((this.rotation + segmentSize/2) % 360) / segmentSize);
this.setStation(index);
}
这个项目做起来非常过瘾,因为它把老派界面的真实触感与最前沿的生成式 AI 结合到了一起。
“幽灵 DJ”的效果确实增加了一层沉浸感,而这是纯生成式音乐应用通常缺少的。它给了 AI 一个真正的声音——字面意义上的声音——也让这个无限电台仿佛活了起来。
你可以在 AI Studio 中查看完整代码:https://aistudio.google.com/apps/drive/1L23eufECJSn0KPta3eAVo-PGdM4iXm-1
如果你也尝试构建自己的电台,记得告诉我!我目前正沉浸在 94.0 的 Shoegaze 电台里。📻
如需采取进一步行动,你可以考虑屏蔽此人和/或举报滥用行为。