feat(livekit): upgrade Gemini starter

This commit is contained in:
Yahya Alhinai
2026-06-28 07:38:09 +00:00
parent c4c65f8802
commit 6083f45f05
7 changed files with 1114 additions and 987 deletions
@@ -2,3 +2,6 @@ LIVEKIT_API_KEY=<your API Key>
LIVEKIT_API_SECRET=<your API Secret> LIVEKIT_API_SECRET=<your API Secret>
LIVEKIT_URL=<your LiveKit server URL> LIVEKIT_URL=<your LiveKit server URL>
GOOGLE_API_KEY=<your Google/Gemini API key> GOOGLE_API_KEY=<your Google/Gemini API key>
GEMINI_REALTIME_MODEL=gemini-3.1-flash-live-preview
GEMINI_IMAGE_MODEL=gemini-3.1-flash-image
GEMINI_LYRIA_MODEL=models/lyria-realtime-exp
@@ -16,9 +16,9 @@ logger = logging.getLogger(__name__)
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
# HACK HERE: swap model IDs to experiment # HACK HERE: swap model IDs to experiment
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
REALTIME_MODEL = "gemini-2.5-flash-native-audio-preview-12-2025" REALTIME_MODEL = os.getenv("GEMINI_REALTIME_MODEL", "gemini-3.1-flash-live-preview")
IMAGE_MODEL = "gemini-2.5-flash-image" # Nano Banana IMAGE_MODEL = os.getenv("GEMINI_IMAGE_MODEL", "gemini-3.1-flash-image") # Nano Banana 2
LYRIA_MODEL = "models/lyria-realtime-exp" LYRIA_MODEL = os.getenv("GEMINI_LYRIA_MODEL", "models/lyria-realtime-exp")
# ───────────────────────────────────────────── # ─────────────────────────────────────────────
# HACK HERE: change the agent's persona # HACK HERE: change the agent's persona
@@ -27,7 +27,7 @@ PERSONA_INSTRUCTIONS = """You are a creative multimodal AI assistant at a Google
You can see through the user's camera, hear them speak, generate images, and play real-time music. You can see through the user's camera, hear them speak, generate images, and play real-time music.
Your capabilities: Your capabilities:
- generate_image: Create images with Nano Banana (Gemini 2.5 Flash Image). Use this when asked to generate, create, render, or visualize anything. - generate_image: Create images with Nano Banana 2 (Gemini 3.1 Flash Image). Use this when asked to generate, create, render, or visualize anything.
- start_music: Play real-time generative music with Lyria RealTime. Use this for soundtracks, ambience, or any audio atmosphere. - start_music: Play real-time generative music with Lyria RealTime. Use this for soundtracks, ambience, or any audio atmosphere.
- stop_music: Stop the current music. - stop_music: Stop the current music.
@@ -52,7 +52,7 @@ When asked about visuals:
self._music_stop_event = asyncio.Event() self._music_stop_event = asyncio.Event()
self._music_track_pub = None self._music_track_pub = None
# Standard client for image generation (NanoBanana 2) # Standard client for image generation (Nano Banana 2)
self._image_client = genai.Client(api_key=os.environ["GOOGLE_API_KEY"]) self._image_client = genai.Client(api_key=os.environ["GOOGLE_API_KEY"])
# v1alpha client required for Lyria RealTime # v1alpha client required for Lyria RealTime
@@ -70,7 +70,7 @@ When asked about visuals:
context: RunContext, context: RunContext,
prompt: str, prompt: str,
) -> str: ) -> str:
"""Generate an image using NanoBanana 2 and display it on the user's screen. """Generate an image using Nano Banana 2 and display it on the user's screen.
Call this whenever the user asks you to create, generate, render, or visualize something. Call this whenever the user asks you to create, generate, render, or visualize something.
@@ -281,12 +281,13 @@ async def entrypoint(ctx: agents.JobContext):
await ctx.connect() await ctx.connect()
try: if REALTIME_MODEL != "gemini-3.1-flash-live-preview":
await session.generate_reply( try:
instructions="Greet the user. Let them know you can generate images with Nano Banana and play real-time music with Lyria. Mention they can enable their camera for visual context." await session.generate_reply(
) instructions="Greet the user. Let them know you can generate images with Nano Banana 2 and play real-time music with Lyria. Mention they can enable their camera for visual context."
except Exception as exc: )
logger.warning("Initial greeting failed: %s", exc) except Exception as exc:
logger.warning("Initial greeting failed: %s", exc)
if __name__ == "__main__": if __name__ == "__main__":
@@ -4,8 +4,8 @@ version = "0.1.0"
description = "Gemini hackathon starter — voice, vision, image generation, and real-time music with LiveKit" description = "Gemini hackathon starter — voice, vision, image generation, and real-time music with LiveKit"
requires-python = ">=3.10,<3.14" requires-python = ">=3.10,<3.14"
dependencies = [ dependencies = [
"livekit-agents[google,images]~=1.4", "livekit-agents[google,images]>=1.6.4,<1.7",
"google-genai>=1.16.0", "google-genai>=2.10.0,<3",
"python-dotenv>=1.0.0", "python-dotenv>=1.0.0",
] ]
File diff suppressed because it is too large Load Diff
@@ -25,7 +25,7 @@ export const APP_CONFIG_DEFAULTS: AppConfig = {
companyName: 'Gemini Hackathon', companyName: 'Gemini Hackathon',
pageTitle: 'Gemini Hacker Starter', pageTitle: 'Gemini Hacker Starter',
pageDescription: pageDescription:
'Voice, vision, image generation, and real-time music with Gemini 2.5 native audio, Nano Banana, and Lyria', 'Voice, vision, image generation, and real-time music with Gemini 3.1 Live, Nano Banana 2, and Lyria',
supportsChatInput: true, supportsChatInput: true,
supportsVideoInput: true, supportsVideoInput: true,
@@ -34,7 +34,7 @@ export const WelcomeView = ({
<WelcomeImage /> <WelcomeImage />
<p className="text-foreground max-w-prose pt-1 leading-6 font-medium"> <p className="text-foreground max-w-prose pt-1 leading-6 font-medium">
Voice, vision, images, and music powered by Gemini 3.1, NanoBanana 2, and Lyria Voice, vision, images, and music powered by Gemini 3.1 Live, NanoBanana 2, and Lyria
</p> </p>
<Button <Button
+27 -2
View File
@@ -116,6 +116,7 @@ export function PodView({
const [testingVoice, setTestingVoice] = useState(false); const [testingVoice, setTestingVoice] = useState(false);
const [note, setNote] = useState<string | null>(null); const [note, setNote] = useState<string | null>(null);
const [audioBlocked, setAudioBlocked] = useState(false); const [audioBlocked, setAudioBlocked] = useState(false);
const [remoteAudioTracks, setRemoteAudioTracks] = useState(0);
const [micOn, setMicOn] = useState(false); const [micOn, setMicOn] = useState(false);
const [leftStreamOpen, setLeftStreamOpen] = useState(() => const [leftStreamOpen, setLeftStreamOpen] = useState(() =>
readStoredBool('podman.myStreamOpen', true), readStoredBool('podman.myStreamOpen', true),
@@ -146,6 +147,7 @@ export function PodView({
element.autoplay = true; element.autoplay = true;
audioElementsRef.current.set(key, element); audioElementsRef.current.set(key, element);
audioRef.current.appendChild(element); audioRef.current.appendChild(element);
setRemoteAudioTracks(audioElementsRef.current.size);
}; };
const removeAudio = (track: RemoteTrack, pub?: RemoteTrackPublication) => { const removeAudio = (track: RemoteTrack, pub?: RemoteTrackPublication) => {
const key = pub?.trackSid || track.sid || track.mediaStreamTrack.id; const key = pub?.trackSid || track.sid || track.mediaStreamTrack.id;
@@ -153,6 +155,7 @@ export function PodView({
if (attached) { if (attached) {
attached.remove(); attached.remove();
audioElementsRef.current.delete(key); audioElementsRef.current.delete(key);
setRemoteAudioTracks(audioElementsRef.current.size);
} }
track.detach().forEach((el) => el.remove()); track.detach().forEach((el) => el.remove());
}; };
@@ -165,14 +168,23 @@ export function PodView({
}); });
}; };
const onAudio = (track: RemoteTrack, pub: RemoteTrackPublication) => attachAudio(track, pub); const onAudio = (track: RemoteTrack, pub: RemoteTrackPublication) => attachAudio(track, pub);
const onAudioGone = (track: RemoteTrack, pub: RemoteTrackPublication) => removeAudio(track, pub); const onAudioGone = (track: RemoteTrack, pub: RemoteTrackPublication) =>
removeAudio(track, pub);
const onDisconnected = () => onLeaveRef.current(); const onDisconnected = () => onLeaveRef.current();
// Browsers block autoplay of incoming audio until a user gesture unlocks it. // Browsers block autoplay of incoming audio until a user gesture unlocks it.
// Surface a button whenever the room can't play sound so PodMan's voice cues // Surface a button whenever the room can't play sound so PodMan's voice cues
// are actually heard. // are actually heard.
const onPlaybackChanged = () => setAudioBlocked(!room.canPlaybackAudio); const onPlaybackChanged = () => setAudioBlocked(!room.canPlaybackAudio);
const unlockAudioFromGesture = () => {
void room.startAudio().finally(() => setAudioBlocked(!room.canPlaybackAudio));
};
onPlaybackChanged(); onPlaybackChanged();
setMicOn(room.localParticipant.isMicrophoneEnabled); setMicOn(room.localParticipant.isMicrophoneEnabled);
window.addEventListener('pointerdown', unlockAudioFromGesture, {
once: true,
capture: true,
});
window.addEventListener('keydown', unlockAudioFromGesture, { once: true, capture: true });
room room
.on(RoomEvent.ParticipantConnected, refresh) .on(RoomEvent.ParticipantConnected, refresh)
@@ -193,8 +205,11 @@ export function PodView({
.off(RoomEvent.TrackUnsubscribed, onAudioGone) .off(RoomEvent.TrackUnsubscribed, onAudioGone)
.off(RoomEvent.AudioPlaybackStatusChanged, onPlaybackChanged) .off(RoomEvent.AudioPlaybackStatusChanged, onPlaybackChanged)
.off(RoomEvent.Disconnected, onDisconnected); .off(RoomEvent.Disconnected, onDisconnected);
window.removeEventListener('pointerdown', unlockAudioFromGesture, { capture: true });
window.removeEventListener('keydown', unlockAudioFromGesture, { capture: true });
audioElementsRef.current.forEach((el) => el.remove()); audioElementsRef.current.forEach((el) => el.remove());
audioElementsRef.current.clear(); audioElementsRef.current.clear();
setRemoteAudioTracks(0);
}; };
}, [room, me]); }, [room, me]);
@@ -268,6 +283,8 @@ export function PodView({
if (!room) return; if (!room) return;
setNote(null); setNote(null);
try { try {
await room.startAudio().catch(() => {});
setAudioBlocked(!room.canPlaybackAudio);
if (sharing) { if (sharing) {
if (screenTrackRef.current) if (screenTrackRef.current)
await room.localParticipant.unpublishTrack(screenTrackRef.current); await room.localParticipant.unpublishTrack(screenTrackRef.current);
@@ -585,7 +602,15 @@ export function PodView({
<StatusLine <StatusLine
label="Audio" label="Audio"
value={ value={
beat.on ? (beat.mine ? 'publishing' : `${beat.by} playing`) : 'ready' audioBlocked
? 'blocked'
: remoteAudioTracks > 0
? `${remoteAudioTracks} remote track${remoteAudioTracks === 1 ? '' : 's'}`
: beat.on
? beat.mine
? 'publishing'
: `${beat.by} playing`
: 'ready'
} }
/> />
<StatusLine label="Agent" value={podmanPresent ? 'watching' : 'waiting'} /> <StatusLine label="Agent" value={podmanPresent ? 'watching' : 'waiting'} />