📻 我用 Gemini + Lyria 打造了一个无限循环的 90 年代复古音响(它还有个 AI DJ!)
我最近做了一个实验,说实话,我简直爱不释手。它是一个基于网络的虚拟音响。你调频,电台切换,音乐实时淡入淡出。
但最关键的是:这位DJ是人工智能。
每次你选定一个电台,一个动态生成的语音(由Gemini 文本转语音技术提供)就会响起,介绍歌曲和类型,而且完全符合上下文语境。这感觉就像机器里的一个幽灵,它是由 Google Gen AI SDK、Lit 和 Lyria 实时模型构建的。
以下是我搭建它的方法。
技术栈
- 氛围编码平台: 谷歌人工智能工作室
- 框架: Lit(Web Components)+ Vite
- 音乐世代:谷歌的
lyria-realtime-exp模型 - DJ语音和脚本: Gemini 2.5 Flash 和 Gemini TTS(Fenrir语音)
- 视觉效果:收音机使用 CSS,背景使用 Gemini 2.5 Flash Image。
1. 无限音乐流🎵
该应用程序的核心是……LiveMusicHelper它与模型连接lyria-realtime-exp。与生成静态 MP3 文件不同,它建立了一个会话,我们可以通过发送“加权提示”来实时控制音乐。
当你在用户界面上转动调谐旋钮时,我们并不是在下载一首新歌;我们是在告诉人工智能转移注意力。
// 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);
可视化旋钮组件将旋转角度映射到提示数组中的索引。如果您位于索引 0,则“Bossa Nova”的权重为1.0。
2. AI DJ(“秘诀”)🎙️
这是我最喜欢的部分。没有DJ告诉你正在播放什么节目,收音机就不能称之为收音机。我RadioAnnouncer为此创建了一个类。
它的运作分为两步:
- 生成脚本:我们要求 Gemini 写一句介绍语。
- 生成音频:我们将该文本传递给 TTS 模型。
第一步:性格
我们要求 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预置配置中的语音。
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 逐个播报。我使用了防抖功能,这样只有在用户停止转动旋钮 800 毫秒后,才会触发信号生成。
3. 美学 🎨
用户界面采用 Lit 构建。音箱本身则结合了 CSS 样式和 SVG 技术来呈现扬声器和旋钮。
扬声器脉冲:
我使用 Web Audio API 创建了一个。我们获取当前频率数据并将其映射到扬声器锥体上的AudioAnalyserCSS 。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.'
4. 应对“舆论风暴”😵💫
这个项目最难的部分之一是调谐旋钮的数学计算。我们需要将鼠标/触摸移动转换为旋转,然后将旋转锁定到特定的“档位”。
我实现了一个循环捕获逻辑:
- 计算旋钮中心与鼠标光标之间的角度。
delta从点击开始计算(变化)。- 处理 0/360 度环绕逻辑,以便您可以无限旋转它。
// 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);
}
太长不看
这个项目的开发过程非常愉快,因为它结合了传统界面的触感和尖端的生成式人工智能。
“幽灵DJ”效果确实增添了一层纯粹的生成式音乐应用通常缺乏的沉浸感。它赋予了人工智能声音——字面意义上的声音——让无限循环的电台音乐听起来鲜活起来。
您可以在 AI Studio 中查看完整代码: https://aistudio.google.com/apps/drive/1L23eufECJSn0KPta3eAVo-PGdM4iXm-1
如果你尝试创建自己的电台,请告诉我!我现在正在听 94.0 “Shoegaze” 频道。📻
文章来源:https://dev.to/googleai/i-built-an-infinite-90s-boombox-with-gemini-lyria-and-it-has-an-ai-dj-3jh8