Skip to content

Commit 7ad9000

Browse files
authored
add gemini demo (#62)
1 parent abad93c commit 7ad9000

37 files changed

Lines changed: 4664 additions & 0 deletions

gemini-demo/.dockerignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
node_modules/
2+
dist/
3+
.env
4+
*.log
5+
.DS_Store
6+
backend/yarn.lock
7+
web/yarn.lock

gemini-demo/.env.example

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
FISHJAM_ID=
2+
FISHJAM_MANAGEMENT_TOKEN=
3+
GEMINI_API_KEY=
4+
VITE_FISHJAM_ID=

gemini-demo/.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
node_modules/
2+
.yarn
3+
dist/
4+
.env
5+
*.log
6+
.DS_Store

gemini-demo/.yarnrc.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
nodeLinker: node-modules

gemini-demo/README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# Gemini Demo
2+
3+
A minimal example of a video call with a Gemini Live AI agent using Fishjam Cloud.
4+
5+
## What it does
6+
7+
- Create a room and join a video call
8+
- Spawn a Gemini Live voice agent with a custom system prompt
9+
- The agent joins the call, listens to participants, and responds with voice
10+
- Supports Google Search for real-time information
11+
12+
## Setup
13+
14+
1. Copy `.env.example` to `.env` and fill in your credentials:
15+
16+
```
17+
cp .env.example .env
18+
```
19+
20+
2. Install dependencies:
21+
22+
```
23+
cd backend && npm install
24+
cd ../web && npm install
25+
```
26+
27+
3. Start the backend:
28+
29+
```
30+
cd backend && npm run start
31+
```
32+
33+
4. Start the frontend (in another terminal):
34+
35+
```
36+
cd web && npm run start
37+
```
38+
39+
5. Open http://localhost:5173
40+
41+
## Architecture
42+
43+
```
44+
backend/src/main.ts - Fastify + tRPC server, Fishjam SDK, Gemini Live API
45+
web/src/App.tsx - React frontend with Fishjam React Client
46+
web/src/trpc.ts - tRPC client setup
47+
```
48+
49+
### Audio flow
50+
51+
```
52+
Peer audio (16kHz) → Fishjam Agent → Gemini Live API
53+
54+
Fishjam Agent Track ← Gemini response (24kHz)
55+
56+
All peers hear the agent
57+
```

gemini-demo/backend/.yarnrc.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
nodeLinker: node-modules

gemini-demo/backend/Dockerfile

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
FROM node:24-alpine AS builder
2+
3+
WORKDIR /app
4+
5+
RUN apk add --no-cache python3 make g++
6+
RUN corepack enable
7+
8+
COPY package.json yarn.lock .yarnrc.yml ./
9+
COPY backend ./backend
10+
COPY web ./web
11+
12+
RUN yarn install --immutable
13+
14+
RUN yarn workspace gemini-demo-backend build
15+
16+
FROM node:24-alpine AS runner
17+
18+
WORKDIR /app
19+
20+
RUN apk add --no-cache dumb-init python3 make g++
21+
RUN corepack enable
22+
23+
COPY package.json yarn.lock .yarnrc.yml ./
24+
COPY backend ./backend
25+
COPY web ./web
26+
27+
RUN yarn install --immutable
28+
29+
COPY --from=builder /app/backend/dist ./backend/dist
30+
31+
RUN addgroup -g 1001 -S nodejs && adduser -S nodejs -u 1001
32+
USER nodejs
33+
34+
EXPOSE 8000
35+
36+
ENTRYPOINT ["dumb-init", "--"]
37+
CMD ["node", "backend/dist/main.js"]

gemini-demo/backend/package.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "gemini-demo-backend",
3+
"private": true,
4+
"version": "0.0.0",
5+
"type": "module",
6+
"exports": {
7+
".": {
8+
"types": "./src/main.ts"
9+
}
10+
},
11+
"types": "./src/main.ts",
12+
"dependencies": {
13+
"@fastify/cors": "^11.1.0",
14+
"@fishjam-cloud/js-server-sdk": "^0.25.4",
15+
"@google/genai": "^1.44.0",
16+
"@trpc/server": "^11.6.0",
17+
"dotenv": "^17.2.3",
18+
"fastify": "^5.6.1",
19+
"pino-pretty": "^13.1.1",
20+
"ws": "^8.18.0",
21+
"zod": "^4.1.11"
22+
},
23+
"devDependencies": {
24+
"@tsconfig/node24": "^24.0.1",
25+
"@types/node": "^24.5.2",
26+
"@types/ws": "^8.18.1",
27+
"tsx": "^4.20.6",
28+
"typescript": "^5.9.2"
29+
},
30+
"scripts": {
31+
"start": "tsx watch src/main.ts",
32+
"build": "tsc -p tsconfig.json",
33+
"typecheck": "tsc --noEmit"
34+
},
35+
"packageManager": "yarn@4.12.0"
36+
}

gemini-demo/backend/src/agents.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { LiveServerMessage, Modality } from "@google/genai";
2+
import * as FishjamGemini from "@fishjam-cloud/js-server-sdk/gemini";
3+
import { type TrackId, type RoomId } from "@fishjam-cloud/js-server-sdk";
4+
5+
import { fishjam, genai } from "./clients.js";
6+
7+
export const createAgent = async (
8+
roomId: RoomId,
9+
systemInstruction: string,
10+
) => {
11+
const { agent: fishjamAgent } = await fishjam.createAgent(roomId, {
12+
output: FishjamGemini.geminiInputAudioSettings,
13+
});
14+
15+
const agentTrack = fishjamAgent.createTrack(
16+
FishjamGemini.geminiOutputAudioSettings,
17+
);
18+
19+
let cleanup: CallableFunction | undefined;
20+
21+
const session = await genai.live.connect({
22+
model: "gemini-3.1-flash-live-preview",
23+
config: {
24+
responseModalities: [Modality.AUDIO],
25+
systemInstruction,
26+
tools: [
27+
{ googleSearch: {} },
28+
{
29+
functionDeclarations: [
30+
{
31+
name: "disconnect",
32+
description: `Disconnect yourself from the room.
33+
Use this when the user asks you to disconnect.`,
34+
},
35+
],
36+
},
37+
],
38+
},
39+
callbacks: {
40+
onmessage: async (message: LiveServerMessage) => {
41+
if (message.data) {
42+
const audio = Buffer.from(message.data, "base64");
43+
fishjamAgent.sendData(agentTrack.id, audio);
44+
}
45+
46+
if (message.serverContent?.interrupted) {
47+
fishjamAgent.interruptTrack(agentTrack.id);
48+
}
49+
50+
message.toolCall?.functionCalls?.forEach((call) => {
51+
if (call.name === "disconnect") {
52+
cleanup?.();
53+
}
54+
});
55+
},
56+
},
57+
});
58+
59+
const room = await fishjam.getRoom(roomId);
60+
61+
const interval = setInterval(async () => {
62+
const humanPeerVideoTrack = room.peers
63+
.find(({ type }) => type === "webrtc")
64+
?.tracks.find(({ type }) => type === "video");
65+
66+
if (!humanPeerVideoTrack?.id) return;
67+
68+
const image = await fishjamAgent.captureImage(
69+
humanPeerVideoTrack.id as TrackId,
70+
);
71+
72+
session.sendRealtimeInput({
73+
video: {
74+
data: Buffer.from(image.data).toString("base64"),
75+
mimeType: image.contentType,
76+
},
77+
});
78+
}, 1000);
79+
80+
fishjamAgent.on("trackData", ({ data }) => {
81+
session.sendRealtimeInput({
82+
audio: {
83+
data: Buffer.from(data).toString("base64"),
84+
mimeType: FishjamGemini.inputMimeType,
85+
},
86+
});
87+
});
88+
89+
cleanup = () => {
90+
clearInterval(interval);
91+
session.close();
92+
fishjamAgent.deleteTrack(agentTrack.id);
93+
fishjamAgent.removeAllListeners("trackData");
94+
fishjamAgent.disconnect();
95+
};
96+
};

gemini-demo/backend/src/clients.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { FishjamClient } from "@fishjam-cloud/js-server-sdk";
2+
import * as FishjamGemini from "@fishjam-cloud/js-server-sdk/gemini";
3+
4+
import config from "./config.js";
5+
6+
export const fishjam = new FishjamClient({
7+
fishjamId: config.FISHJAM_ID,
8+
managementToken: config.FISHJAM_MANAGEMENT_TOKEN,
9+
});
10+
11+
export const genai = FishjamGemini.createClient({
12+
apiKey: config.GEMINI_API_KEY,
13+
});

0 commit comments

Comments
 (0)