Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 22 additions & 15 deletions ai/ai-samples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ This repository demonstrates the following capabilities:
* Multimodal
* Structured Output
* Function Calling
* Automatic Function Calling
* Image Generation
* Video Analysis

## Setup & Configuration

Expand All @@ -21,6 +23,7 @@ To connect this sample app to your Firebase project, register a new Web App in y
1. Navigate to this directory and install dependencies:
```bash
npm install
```

2. Add your Firebase config

Expand All @@ -31,34 +34,38 @@ Copy the example config file and fill in your project values. Open src/config/fi
3. Running the samples

For a full app experience to browse all features:
npm run dev
```bash
npm run dev
```

To run indivual features in isolated mode, run the single feature directly without the app shell using one of these scripts:
To run individual features in isolated mode, run the single feature directly without the app shell using one of these scripts:

npm run dev:text #Text Generation
npm run dev:chat #Chat
npm run dev:multimodal #Multimodal
npm run dev:structured #Structured Output
npm run dev:function #Function Calling
npm run dev:image #Image Generation
```bash
npm run dev:text # Text Generation
npm run dev:chat # Chat
npm run dev:multimodal # Multimodal
npm run dev:structured # Structured Output
npm run dev:function # Function Calling
npm run dev:auto-function # Automatic Function Calling
npm run dev:image # Image Generation
npm run dev:video # Video Analysis

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think package.json needs to be updated to include this script?

```

After running any of the above commands, open your browser to https://localhost:*** (provided in the console)
After running any of the above commands, open your browser to http://localhost:*** (provided in the console)

## Copy service.ts for platform agnostic use

All AI logic is decoupled from the React UI. If you want to use these features in your own project, navigate to any src/features/*/service.ts file. These files are framework-agnostic and can be safely copy-pasted into any JavaScript or TypeScript web project.



## App Check

App check protects your API Key from unauthorized use. It is not required to run the samples locally but highly recommended before deployig to production.
App check protects your API Key from unauthorized use. It is not required to run the samples locally but highly recommended before deploying to production.

Debug token:
firebaseAIService.ts includes App Check intilization for local development. To enable it:
firebaseAIService.ts includes App Check initialization for local development. To enable it:

1. Set VITE_APPCHECK_DEBUG_TOKEN=true in your .env.local file
2. On the first run, a deug token will be printed in the browser console.
2. On the first run, a debug token will be printed in the browser console.
3. Copy that token and register it in the Firebase Console under
App Check -> Apps -> your apps -> Debug Token

Expand All @@ -69,4 +76,4 @@ For production, use reCAPTCHA v3 as the App Check provider:
2. Choose reCaptcha v3 and follow the setup steps
3. Add your reCaptcha site key to firebase-config.ts

See the [App Check Docs](https://firebase.google.com/docs/app-check/web/recaptcha-provider) for full instruction.
See the [App Check Docs](https://firebase.google.com/docs/app-check/web/recaptcha-provider) for full instructions.
3 changes: 1 addition & 2 deletions ai/ai-samples/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@ const NAV_ITEMS = [
{ path: '/function-calling', label: 'Function Calling' },
{ path: '/automatic-function-calling', label: 'Automatic Function Calling' },
{ path: '/image-generation', label: 'Image Generation' },

{ path: '/video-analysis', label: 'Video Analysis' },
];

export default function App() {
const { pathname } = useLocation();


return (
<div className="app-shell">
<nav className="sidebar">
Expand Down
123 changes: 123 additions & 0 deletions ai/ai-samples/src/features/video-analysis/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { useState, useRef } from 'react';
import { analyzeVideo, streamVideoAnalysis } from './service';

export default function VideoAnalysisView() {
const [prompt, setPrompt] = useState('Describe what is happening in this video in detail.');
const [useStreaming, setUseStreaming] = useState(true);
const [response, setResponse] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const fileInputRef = useRef<HTMLInputElement>(null);

const handleAnalyze = async () => {
const fileArray = Array.from(fileInputRef.current?.files ?? []);

if (fileArray.length === 0) {
setError('Please select a video file.');
return;
}

const cleanedPrompt = prompt.trim();
if (!cleanedPrompt) {
setError('Please enter a prompt.');
return;
}

setLoading(true);
setError(null);
setResponse('');

try {
if (useStreaming) {
await streamVideoAnalysis(cleanedPrompt, fileArray[0], (chunk) => {
setResponse((prev) => prev + chunk);
});
} else {
const resultText = await analyzeVideo(cleanedPrompt, fileArray[0]);
setResponse(resultText);
}
} catch (err: unknown) {
const message =
err instanceof Error
? err.message
: 'An unexpected error occurred during video analysis.';
setError(message);
} finally {
setLoading(false);
}
};

return (
<div style={{ padding: '20px', maxWidth: '600px', margin: '0 auto' }}>
<h2>Video Analysis</h2>
<p style={{ color: '#666', marginBottom: '20px' }}>
Upload a video file alongside a text prompt to analyze its content.
</p>

<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
Upload Video:
</label>
<input
type="file"
ref={fileInputRef}
accept="video/*"
style={{ width: '100%', padding: '8px' }}
/>
</div>

<div style={{ marginBottom: '15px' }}>
<label style={{ display: 'block', marginBottom: '5px', fontWeight: 'bold' }}>
Prompt:
</label>
<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
rows={4}
style={{ width: '100%', padding: '8px', fontFamily: 'inherit' }}
/>
</div>

<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '15px' }}>
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
<input
type="checkbox"
checked={useStreaming}
onChange={(e) => setUseStreaming(e.target.checked)}
disabled={loading}
/>
Stream response
</label>

<button
onClick={handleAnalyze}
disabled={loading}
style={{
padding: '10px 20px',
cursor: loading ? 'not-allowed' : 'pointer',
backgroundColor: loading ? '#ccc' : '#007BFF',
color: '#fff',
border: 'none',
borderRadius: '4px',
}}
>
{loading ? 'Analyzing Video...' : 'Analyze Video'}
</button>
</div>

{error && (
<div style={{ color: '#D8000C', backgroundColor: '#FFD2D2', padding: '10px', marginTop: '15px', borderRadius: '4px' }}>
<strong>Error:</strong> {error}
</div>
)}

{response && (
<div style={{ marginTop: '20px', borderTop: '1px solid #eee', paddingTop: '15px' }}>
<h3>Response:</h3>
<p style={{ whiteSpace: 'pre-wrap', lineHeight: '1.5' }}>{response}</p>
</div>
)}
</div>
);
}
89 changes: 89 additions & 0 deletions ai/ai-samples/src/features/video-analysis/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { getAiModel } from '../../services/firebaseAIService';
import { Part } from 'firebase/ai';

/**
* Converts a standard browser File object (Video) into a Firebase AI SDK Part.
* Uses the native browser FileReader API to extract the Base64 string.
* @param file The DOM File object from an <input type="file">
* @returns A Promise that resolves to an InlineData Part object
*/
export async function fileToGenerativePart(file: File): Promise<Part> {
return new Promise((resolve, reject) => {
const reader = new FileReader();

reader.onload = () => {
const result = reader.result;

if (typeof result !== 'string') {
return reject(new Error('Failed to parse video file data as Base64.'));
}
const splitResult = result.split(',');
const base64Data = splitResult[1];
if (!base64Data) {
return reject(new Error('Failed to extract Base64 data from file.'));
}
resolve({
inlineData: {
data: base64Data,
mimeType: file.type || 'video/mp4',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is the fallback video/mp4? If there's no file.type isn't something wrong? Or do mp4s sometimes not have a file type?

},
});
};

reader.onerror = () => reject(reader.error || new Error('Error reading video file.'));
reader.readAsDataURL(file);
});
}

// Note: The Gemini API restricts the total size of inline data payloads.
// To process larger files without hitting HTTP 413 errors (PayLoad Too Large),
// See the official Firebase AI documentation for current file size limits and workarounds:
// https://firebase.google.com/docs/ai-logic/solutions/cloud-storage

/**
* Sends a video analysis request (prompt + video file) to the Gemini model.
* @param prompt The string instruction sent to the model (e.g. "Describe what happens in this video").
* @param videoFile The DOM File object containing the video.
* @returns The text string response generated by the model.
*/
export async function analyzeVideo(prompt: string, videoFile: File): Promise<string> {
try {
const videoPart = await fileToGenerativePart(videoFile);
const model = getAiModel('gemini-3.7-flash');
const result = await model.generateContent([prompt, videoPart]);
return result.response.text();
} catch (error: unknown) {
throw error instanceof Error
? error
: new Error('An unknown error occurred during video analysis.');
}
}

/**
* Streams the video analysis response from the Gemini model in real-time chunks.
* @param prompt The string instruction sent to the model.
* @param videoFile The DOM File object containing the video.
* @param onChunk Callback fired whenever a new text chunk is received.
*/
export async function streamVideoAnalysis(
prompt: string,
videoFile: File,
onChunk: (chunk: string) => void
): Promise<void> {
try {
const videoPart = await fileToGenerativePart(videoFile);
const model = getAiModel('gemini-3.7-flash');
const result = await model.generateContentStream([prompt, videoPart]);

for await (const chunk of result.stream) {
const chunkText = chunk.text();
if (chunkText) {
onChunk(chunkText);
}
}
} catch (error: unknown) {
throw error instanceof Error
? error
: new Error('An unknown error occurred during video streaming.');
}
}
9 changes: 7 additions & 2 deletions ai/ai-samples/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import StructuredOutput from './features/structured-output';
import FunctionCalling from './features/function-calling';
import ImageGeneration from './features/image-generation';
import AutomaticFunctionCalling from './features/automatic-function-calling';

import VideoAnalysis from './features/video-analysis';

const router = createBrowserRouter([
{
Expand All @@ -24,9 +24,10 @@ const router = createBrowserRouter([
{ path: 'function-calling', element: <FunctionCalling /> },
{ path: 'automatic-function-calling', element: <AutomaticFunctionCalling /> },
{ path: 'image-generation', element: <ImageGeneration /> },

{ path: 'video-analysis', element: <VideoAnalysis /> },
],
},

]);

const isolatedFeature = import.meta.env.VITE_ISOLATED_FEATURE;
Expand All @@ -46,6 +47,10 @@ const renderContent = () => {
return <FunctionCalling />;
case 'image-generation':
return <ImageGeneration />;
case 'automatic-function-calling':
return <AutomaticFunctionCalling />;
case 'video-anaylsis':
return <VideoAnalysis />;
default:
return <RouterProvider router={router} />;
}
Expand Down
Loading