├── static
├── uploads
│ └── .gitkeep
├── generated
│ └── .gitkeep
├── js
│ └── script.js
└── css
│ ├── layout.css
│ └── style.css
├── requirements.txt
├── .env.example
├── .gitignore
├── README.md
├── templates
├── image_editing.html
├── bounding_box.html
├── image_generation.html
├── image_segmentation.html
├── index.html
├── image_qa.html
└── settings.html
├── doc.md
└── app.py
/static/uploads/.gitkeep:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/static/generated/.gitkeep:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | flask
2 | google-genai
3 | Pillow
4 | python-dotenv
5 | requests
6 |
--------------------------------------------------------------------------------
/.env.example:
--------------------------------------------------------------------------------
1 | # Gemini API密钥
2 | # 从 https://aistudio.google.com/app/apikey 获取
3 | GEMINI_API_KEY=your_api_key_here
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Python
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 | *.so
6 | .Python
7 | env/
8 | build/
9 | develop-eggs/
10 | dist/
11 | downloads/
12 | eggs/
13 | .eggs/
14 | lib/
15 | lib64/
16 | parts/
17 | sdist/
18 | var/
19 | *.egg-info/
20 | .installed.cfg
21 | *.egg
22 |
23 | # Virtual Environment
24 | venv/
25 | ENV/
26 | env/
27 |
28 | # Flask
29 | instance/
30 | .webassets-cache
31 |
32 | # Environment variables
33 | .env
34 |
35 | # API keys
36 | api_key.json
37 |
38 | # Uploaded and generated files
39 | static/uploads/*
40 | static/generated/*
41 | !static/uploads/.gitkeep
42 | !static/generated/.gitkeep
43 |
44 | # IDE
45 | .idea/
46 | .vscode/
47 | *.swp
48 | *.swo
49 |
50 | # OS
51 | .DS_Store
52 | Thumbs.db
53 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Gemini 图像应用
2 |
3 | 这是一个基于Google Gemini API的图像处理Flask应用,提供多种图像相关功能。
4 |
5 | ## 功能
6 |
7 | 1. **图像问答** - 上传图片并提问关于图片内容的问题
8 | 2. **图像生成** - 使用Gemini或Imagen生成图像
9 | 3. **图像编辑** - 上传图片并进行编辑
10 | 4. **边界框检测** - 检测图像中的对象并显示边界框
11 | 5. **图像分割** - 对图像进行分割并显示分割结果
12 |
13 | ## 安装
14 |
15 | 1. 克隆仓库
16 | ```
17 | git clone https://github.com/nicekate/gemini-image.git
18 | cd gemini-image
19 | ```
20 |
21 | 2. 创建虚拟环境并激活
22 | ```
23 | python -m venv venv
24 | source venv/bin/activate # Linux/Mac
25 | venv\Scripts\activate # Windows
26 | ```
27 |
28 | 3. 安装依赖
29 | ```
30 | pip install -r requirements.txt
31 | ```
32 |
33 | 4. 配置API密钥
34 | - 复制`.env.example`文件并重命名为`.env`
35 | ```
36 | cp .env.example .env
37 | ```
38 | - 在`.env`文件中设置你的Gemini API密钥
39 | ```
40 | GEMINI_API_KEY=your_api_key_here
41 | ```
42 | - 你也可以在应用运行后通过设置页面配置API密钥
43 |
44 | ## 运行应用
45 |
46 | ```
47 | python app.py
48 | ```
49 |
50 | 然后在浏览器中访问 `http://127.0.0.1:5000/`
51 |
52 | ## 技术栈
53 |
54 | - Flask - Web框架
55 | - Google Generative AI (Gemini API) - AI模型
56 | - Pillow - 图像处理
57 | - Bootstrap - 前端样式
58 |
59 | ## 注意事项
60 |
61 | - 需要有效的Google Gemini API密钥
62 | - 图像生成功能中的Imagen模型仅在付费层级可用
63 | - 图像分割功能需要使用Gemini 2.5 Pro实验版模型
64 |
65 | ## 隐私与安全
66 |
67 | - API密钥存储在两个位置:`.env`文件和应用内部的`api_key.json`文件
68 | - 这两个文件都已添加到`.gitignore`中,不会被提交到代码库
69 | - 请勿在公共场合分享您的API密钥
70 | - 应用中的设置页面提供了安全的API密钥管理功能
71 |
72 | ## 许可证
73 |
74 | MIT
75 |
76 | ## 项目仓库
77 |
78 | [GitHub: https://github.com/nicekate/gemini-image](https://github.com/nicekate/gemini-image)
79 |
--------------------------------------------------------------------------------
/static/js/script.js:
--------------------------------------------------------------------------------
1 | // 页面加载完成后执行
2 | document.addEventListener('DOMContentLoaded', function() {
3 | // 图片上传预览
4 | const imageInputs = document.querySelectorAll('input[type="file"][accept*="image"]');
5 |
6 | imageInputs.forEach(input => {
7 | input.addEventListener('change', function(event) {
8 | const file = event.target.files[0];
9 | if (file) {
10 | const reader = new FileReader();
11 |
12 | reader.onload = function(e) {
13 | // 查找最近的预览容器
14 | const previewContainer = input.parentElement.querySelector('.image-preview');
15 |
16 | if (previewContainer) {
17 | // 如果已有预览容器,更新它
18 | const img = previewContainer.querySelector('img');
19 | img.src = e.target.result;
20 | } else {
21 | // 否则创建新的预览容器
22 | const preview = document.createElement('div');
23 | preview.className = 'image-preview mt-2';
24 |
25 | const img = document.createElement('img');
26 | img.src = e.target.result;
27 | img.className = 'img-fluid rounded';
28 | img.style.maxHeight = '200px';
29 |
30 | preview.appendChild(img);
31 | input.parentElement.appendChild(preview);
32 | }
33 | };
34 |
35 | reader.readAsDataURL(file);
36 | }
37 | });
38 | });
39 |
40 | // 表单提交时显示加载动画
41 | const forms = document.querySelectorAll('form');
42 |
43 | forms.forEach(form => {
44 | form.addEventListener('submit', function() {
45 | // 检查是否已有加载动画
46 | let loadingElement = form.querySelector('.loading');
47 |
48 | if (!loadingElement) {
49 | // 创建加载动画
50 | loadingElement = document.createElement('div');
51 | loadingElement.className = 'loading mt-3';
52 |
53 | const spinner = document.createElement('div');
54 | spinner.className = 'loading-spinner';
55 |
56 | const text = document.createElement('p');
57 | text.className = 'mt-2';
58 | text.textContent = '处理中,请稍候...';
59 |
60 | loadingElement.appendChild(spinner);
61 | loadingElement.appendChild(text);
62 |
63 | // 添加到表单底部
64 | form.appendChild(loadingElement);
65 | }
66 |
67 | // 显示加载动画
68 | loadingElement.style.display = 'block';
69 |
70 | // 禁用提交按钮
71 | const submitButton = form.querySelector('button[type="submit"]');
72 | if (submitButton) {
73 | submitButton.disabled = true;
74 | submitButton.innerHTML = ' 处理中...';
75 | }
76 | });
77 | });
78 |
79 | // 图像对比滑块(如果存在)
80 | const imageCompareSliders = document.querySelectorAll('.image-compare-slider');
81 |
82 | imageCompareSliders.forEach(slider => {
83 | slider.addEventListener('input', function() {
84 | const beforeImage = this.parentElement.querySelector('.before-image');
85 | const afterImage = this.parentElement.querySelector('.after-image');
86 |
87 | if (beforeImage && afterImage) {
88 | afterImage.style.clipPath = `inset(0 0 0 ${this.value}%)`;
89 | }
90 | });
91 | });
92 | });
93 |
--------------------------------------------------------------------------------
/static/css/layout.css:
--------------------------------------------------------------------------------
1 | /* 宽松布局样式 */
2 | :root {
3 | --spacer-sm: 1rem;
4 | --spacer-md: 2rem;
5 | --spacer-lg: 3rem;
6 | --spacer-xl: 5rem;
7 | --primary-color: #4285f4;
8 | --secondary-color: #34a853;
9 | --accent-color: #ea4335;
10 | --neutral-color: #fbbc05;
11 | }
12 |
13 | /* 增加容器宽度 */
14 | .container-wide {
15 | max-width: 1400px !important;
16 | padding: 0 var(--spacer-md);
17 | }
18 |
19 | /* 页面标题区域 */
20 | .page-header {
21 | margin: 0 0 var(--spacer-xl) 0;
22 | padding: var(--spacer-lg) 0;
23 | background: linear-gradient(135deg, var(--primary-color) 0%, var(--secondary-color) 100%);
24 | color: white;
25 | border-radius: 0 0 30px 30px;
26 | box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
27 | position: relative;
28 | }
29 |
30 | .page-header::after {
31 | content: '';
32 | position: absolute;
33 | bottom: -20px;
34 | left: 50%;
35 | transform: translateX(-50%);
36 | width: 60px;
37 | height: 6px;
38 | background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
39 | border-radius: 3px;
40 | }
41 |
42 | .page-header h1 {
43 | font-size: 2.8rem;
44 | font-weight: 700;
45 | margin-bottom: var(--spacer-sm);
46 | text-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
47 | }
48 |
49 | .page-header .lead {
50 | font-size: 1.25rem;
51 | max-width: 800px;
52 | margin: 0 auto var(--spacer-md) auto;
53 | opacity: 0.9;
54 | }
55 |
56 | .page-header .btn-outline-secondary {
57 | color: white;
58 | border-color: rgba(255, 255, 255, 0.5);
59 | background-color: rgba(255, 255, 255, 0.1);
60 | }
61 |
62 | .page-header .btn-outline-secondary:hover {
63 | background-color: rgba(255, 255, 255, 0.2);
64 | border-color: white;
65 | color: white;
66 | }
67 |
68 | /* 卡片样式 */
69 | .card-spacious {
70 | border-radius: 16px;
71 | margin-bottom: var(--spacer-lg);
72 | box-shadow: 0 10px 30px rgba(0, 0, 0, 0.05);
73 | border: none;
74 | background-color: white;
75 | overflow: hidden;
76 | }
77 |
78 | /* 表单卡片 */
79 | .col-lg-7 .card-spacious .card-header {
80 | padding: var(--spacer-md);
81 | background-color: var(--secondary-color);
82 | color: white;
83 | border-bottom: none;
84 | position: relative;
85 | font-weight: 600;
86 | }
87 |
88 | .col-lg-7 .card-spacious .card-header::after {
89 | content: '';
90 | position: absolute;
91 | bottom: 0;
92 | left: 0;
93 | width: 100%;
94 | height: 4px;
95 | background: linear-gradient(90deg, var(--primary-color), var(--secondary-color));
96 | opacity: 0.7;
97 | }
98 |
99 | /* 结果卡片 */
100 | .col-lg-5 .card-spacious .card-header {
101 | padding: var(--spacer-md);
102 | background-color: var(--primary-color);
103 | color: white;
104 | border-bottom: none;
105 | position: relative;
106 | font-weight: 600;
107 | }
108 |
109 | .col-lg-5 .card-spacious .card-header::after {
110 | content: '';
111 | position: absolute;
112 | bottom: 0;
113 | left: 0;
114 | width: 100%;
115 | height: 4px;
116 | background: linear-gradient(90deg, var(--secondary-color), var(--primary-color));
117 | opacity: 0.7;
118 | }
119 |
120 | .card-spacious .card-header .card-title {
121 | margin-bottom: 0;
122 | font-size: 1.4rem;
123 | font-weight: 600;
124 | }
125 |
126 | .card-spacious .card-body {
127 | padding: var(--spacer-lg);
128 | background-color: white;
129 | }
130 |
131 | .card-spacious .card-title {
132 | font-size: 1.5rem;
133 | font-weight: 600;
134 | margin-bottom: 0;
135 | }
136 |
137 | /* 表单元素 */
138 | .form-spacious .form-label {
139 | font-weight: 500;
140 | margin-bottom: 0.75rem;
141 | font-size: 1.1rem;
142 | }
143 |
144 | .form-spacious .form-control {
145 | padding: 0.75rem 1rem;
146 | font-size: 1.1rem;
147 | border-radius: 10px;
148 | border: 1px solid #dee2e6;
149 | margin-bottom: var(--spacer-md);
150 | }
151 |
152 | .form-spacious textarea.form-control {
153 | min-height: 150px;
154 | }
155 |
156 | .form-spacious .form-text {
157 | margin-top: 0.5rem;
158 | font-size: 0.9rem;
159 | }
160 |
161 | .form-spacious .form-check {
162 | margin-bottom: 1rem;
163 | }
164 |
165 | /* 按钮样式 */
166 | .btn-spacious {
167 | padding: 0.75rem 2rem;
168 | font-size: 1.1rem;
169 | border-radius: 10px;
170 | font-weight: 500;
171 | }
172 |
173 | /* 结果区域 */
174 | .result-section {
175 | margin-top: var(--spacer-md);
176 | }
177 |
178 | .result-section img {
179 | max-width: 100%;
180 | border-radius: 10px;
181 | margin-bottom: var(--spacer-md);
182 | }
183 |
184 | /* 响应式调整 */
185 | @media (min-width: 992px) {
186 | .row-spacious {
187 | margin-left: -1.5rem;
188 | margin-right: -1.5rem;
189 | }
190 |
191 | .row-spacious > [class*="col-"] {
192 | padding-left: 1.5rem;
193 | padding-right: 1.5rem;
194 | }
195 | }
196 |
197 | /* 两列布局变为单列 */
198 | @media (max-width: 991.98px) {
199 | .single-column-layout .row > [class*="col-"] {
200 | width: 100%;
201 | max-width: 100%;
202 | flex: 0 0 100%;
203 | }
204 | }
205 |
--------------------------------------------------------------------------------
/templates/image_editing.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 图像编辑 - Gemini 图像应用
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
21 |
22 | {% if error %}
23 |
24 | {{ error }}
25 |
26 | {% endif %}
27 |
28 |
29 |
53 |
54 |
55 | {% if original_image and edited_image %}
56 |
57 |
60 |
61 |
70 |
79 |
80 |
编辑指令:
81 |
{{ edit_prompt }}
82 |
83 | {% if description %}
84 |
85 |
描述:
86 |
87 | {{ description|safe }}
88 |
89 |
90 | {% endif %}
91 |
96 |
97 |
98 | {% endif %}
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
--------------------------------------------------------------------------------
/templates/bounding_box.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 边界框检测 - Gemini 图像应用
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
21 |
22 | {% if error %}
23 |
24 | {{ error }}
25 |
26 | {% endif %}
27 |
28 |
29 |
30 |
31 |
34 |
35 |
50 |
51 |
52 |
53 |
54 |
55 | {% if image_path and bbox_image %}
56 |
57 |
60 |
61 |
70 |
79 |
80 |
检测对象:
81 |
{{ detect_object if detect_object else '所有对象' }}
82 |
83 | {% if bboxes %}
84 |
85 |
边界框坐标:
86 |
87 | {% for bbox in bboxes %}
88 |
边界框 {{ loop.index }}: [{{ bbox.coordinates|join(', ') }}]
89 | {% endfor %}
90 |
91 |
92 | {% endif %}
93 |
94 |
原始响应:
95 |
{{ raw_response }}
96 |
97 |
102 |
103 |
104 | {% endif %}
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
--------------------------------------------------------------------------------
/templates/image_generation.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 图像生成 - Gemini 图像应用
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
21 |
22 | {% if error %}
23 |
24 | {{ error }}
25 |
26 | {% endif %}
27 |
28 |
29 |
30 |
31 |
34 |
35 |
61 |
62 |
63 |
64 |
65 |
66 | {% if image_path %}
67 |
68 |
71 |
72 |
80 |
81 |
提示词:
82 |
{{ prompt }}
83 |
84 | {% if description %}
85 |
86 |
描述:
87 |
88 | {{ description|safe }}
89 |
90 |
91 | {% endif %}
92 |
93 |
使用模型:
94 |
{{ "Gemini" if model_type == "gemini" else "Imagen" }}
95 |
96 |
101 |
102 |
103 | {% endif %}
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
--------------------------------------------------------------------------------
/templates/image_segmentation.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 图像分割 - Gemini 图像应用
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
21 |
22 | {% if error %}
23 |
24 | {{ error }}
25 |
26 | {% endif %}
27 |
28 |
29 |
30 |
31 |
34 |
35 |
50 |
51 |
52 |
53 |
54 |
55 | {% if image_path and segmented_image %}
56 |
57 |
60 |
61 |
70 |
79 |
88 |
89 |
分割对象:
90 |
{{ segment_object if segment_object else '所有对象' }}
91 |
92 |
93 |
96 |
97 |
{{ raw_response }}
98 |
99 |
100 |
105 |
106 |
107 | {% endif %}
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
--------------------------------------------------------------------------------
/templates/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Gemini 图像应用
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
23 |
24 |
25 | Gemini 图像应用
26 |
27 | 基于Google Gemini API的图像处理应用
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
AI 图像处理功能
36 |
37 |
38 |
51 |
52 |
65 |
66 |
79 |
80 |
93 |
94 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
更多功能
114 |
更多强大的AI图像处理功能正在开发中,敬请期待
115 |
即将推出
116 |
117 |
118 |
119 |
120 |
121 |
122 |
132 |
133 |
134 |
135 |
136 |
137 |
--------------------------------------------------------------------------------
/templates/image_qa.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 图像问答 - Gemini 图像应用
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
21 |
22 | {% if error %}
23 |
24 | {{ error }}
25 |
26 | {% endif %}
27 |
28 |
29 |
30 |
31 |
34 |
35 |
50 |
51 |
52 |
53 |
54 |
55 | {% if image_path %}
56 |
57 |
63 |
64 |
65 |
 }})
66 |
67 |
68 |
69 |
问题:
70 |
73 |
74 |
{{ question }}
75 |
76 |
77 |
78 |
回答:
79 |
82 |
83 |
84 | {{ answer|safe }}
85 |
86 |
87 |
88 |
89 | {% endif %}
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
136 |
137 |
138 |
--------------------------------------------------------------------------------
/doc.md:
--------------------------------------------------------------------------------
1 | # Gemini by Example
2 |
3 | > Gemini is Google's most capable AI model for generating text, code, images, and more. Please visit the [official documentation](https://ai.google.dev/gemini-api/docs) to learn more.
4 |
5 | Gemini by Example is a hands-on introduction to Google's Gemini SDK and API using annotated code examples. This site takes inspiration from [gobyexample.com](https://gobyexample.com), from which I learned many things about the Go programming language.
6 |
7 | Examples here assume Python `>=3.9` and the latest version of the Gemini SDK/API (the [`google-genai`](https://pypi.org/project/google-genai/) package). Try to upgrade to the latest versions if something isn't working.
8 |
9 | Note: A more comprehensive version of this file / documentation is available at [https://geminibyexample.com/llms-ctx.txt](https://geminibyexample.com/llms-ctx.txt), which contains the full text of all examples including code samples and terminal output. You could paste that into Cursor or Gemini or another IDE or AI model to then ask questions and get it to generate code with the Gemini examples as its context.
10 |
11 | ## Text
12 |
13 | - [Simple text generation](https://geminibyexample.com/001-basic-generation/)
14 | - [Streaming text](https://geminibyexample.com/002-streaming-text/): This example demonstrates how to use the Gemini API to generate text content and stream the output..
15 | - [System prompt](https://geminibyexample.com/003-system-prompt/): This example demonstrates how to use system instructions to guide the model's behavior..
16 | - [Reasoning models](https://geminibyexample.com/019-reasoning-models/): This example demonstrates how to access the reasoning trace of a Gemini model and then the final text output.
17 | - [Structured output](https://geminibyexample.com/020-structured-output/): This example shows how to generate structured data using a pydantic model to represent Cats with name, colour, and special ability..
18 |
19 | ## Images
20 |
21 | - [Image question answering](https://geminibyexample.com/004-image-q-a/): This example demonstrates how to use the Gemini API to analyze or understand images of cats, including using image URLs and base64 encoding..
22 | - [Image generation (Gemini and Imagen)](https://geminibyexample.com/005-image-generation/): This example demonstrates generating images using both Gemini 2.0 Flash and Imagen 3 models, focusing on cat-related prompts..
23 | - [Edit an image](https://geminibyexample.com/006-editing-images/): This example demonstrates how to edit an existing image of a cat to add a hat using the Gemini API..
24 | - [Bounding boxes](https://geminibyexample.com/007-bounding-boxes/): This example demonstrates how to use the Gemini API to detect an object (a cat) in an image and retrieve its bounding box coordinates..
25 | - [Image segmentation](https://geminibyexample.com/008-image-segmentation/): This example demonstrates how to use the Gemini API to perform image segmentation on a picture of a cat..
26 |
27 | ## Audio
28 |
29 | - [Audio question answering](https://geminibyexample.com/009-audio-q-a/): This example demonstrates how to ask a question about the content of an audio file using the Gemini API..
30 | - [Audio transcription](https://geminibyexample.com/010-audio-transcription/): This example demonstrates how to transcribe an audio file by providing the audio data inline with the request..
31 | - [Audio summarization](https://geminibyexample.com/011-audio-summarization/): This example demonstrates how to summarize the content of an audio file using the Gemini API..
32 |
33 | ## Video
34 |
35 | - [Video question answering](https://geminibyexample.com/012-video-q-a/): This example demonstrates how to ask questions about a video using the Gemini API.
36 | - [Video summarization](https://geminibyexample.com/013-video-summarization/): This example demonstrates how to summarize the content of a video using the Gemini API.
37 | - [Video transcription](https://geminibyexample.com/014-video-transcription/): This example demonstrates how to transcribe the content of a video using the Gemini API.
38 | - [YouTube video summarization](https://geminibyexample.com/015-youtube-video-summarization/): This example demonstrates how to summarize a YouTube video using its URL..
39 |
40 | ## PDFs and other data types
41 |
42 | - [PDF and CSV data analysis and summarization](https://geminibyexample.com/016-pdf-csv-analysis/): This example demonstrates how to use the Gemini API to analyze data from PDF and CSV files..
43 | - [Translate documents](https://geminibyexample.com/017-content-translation/): This example demonstrates how to load content from a URL and translate it into Chinese using the Gemini API.
44 | - [Extract structured data from a PDF](https://geminibyexample.com/018-structured-data-extraction/): This example demonstrates how to extract structured data from a PDF invoice using the Gemini API and Pydantic..
45 |
46 | ## Agentic behaviour
47 |
48 | - [Function calling & tool use](https://geminibyexample.com/021-tool-use-function-calling/): This example demonstrates how to use the Gemini API to call external functions..
49 | - [Code execution](https://geminibyexample.com/022-code-execution/): This example demonstrates how to use the Gemini API to execute code (agent-style) and calculate the sum of the first 50 prime numbers..
50 | - [Model Context Protocol](https://geminibyexample.com/023-mcp-model-context-protocol/): This example demonstrates using a local MCP server with Gemini to get weather information..
51 | - [Grounded responses with search tool](https://geminibyexample.com/024-grounded-responses/): This example demonstrates how to use the Gemini API with the Search tool to get grounded responses.
52 |
53 | ## Token counting & context windows
54 |
55 | - [Model context windows](https://geminibyexample.com/025-model-context-windows/): This example demonstrates how to access the input and output token limits for different Gemini models..
56 | - [Counting chat tokens](https://geminibyexample.com/026-token-counting/): This example demonstrates how to count tokens in a chat history with the Gemini API, incorporating a cat theme..
57 | - [Calculating multimodal input tokens](https://geminibyexample.com/027-calculate-input-tokens/): This example demonstrates how to calculate input tokens for different data types when using the Gemini API, including images, video, and audio..
58 | - [Context caching](https://geminibyexample.com/028-context-caching/): This example demonstrates how to use the Gemini API's context caching feature to efficiently query a large document multiple times without resending it with each request.
59 |
60 | ## Miscellaneous
61 |
62 | - [Rate limits and retries](https://geminibyexample.com/029-rate-limits-retries/): This example demonstrates generating text with the Gemini API, handling rate limiting errors, and using exponential backoff for retries..
63 | - [Concurrent requests and generation](https://geminibyexample.com/030-async-requests/): This example demonstrates how to generate text using concurrent.futures to make parallel requests to the Gemini API, with a focus on cat-related prompts..
64 | - [Embeddings generation](https://geminibyexample.com/031-embeddings/): This example demonstrates generating text embeddings for cat-related terms using the Gemini API..
65 | - [Safety settings and filters](https://geminibyexample.com/032-safety-filters/): This example demonstrates how to adjust safety settings to block content based on the probability of unsafe content..
66 | - [LiteLLM](https://geminibyexample.com/033-litellm/): This example demonstrates how to use the LiteLLM library to make calls to the Gemini API.
67 |
68 | ## Resources
69 |
70 | - [Official Gemini Documentation](https://ai.google.dev/gemini-api/docs)
71 | - [Source Code](https://github.com/strickvl/geminibyexample)
72 | - [Author's Blog](https://mlops.systems)
73 | - [Author's LinkedIn](https://linkedin.com/in/strickvl)
74 |
75 | Last updated: April 6, 2025
--------------------------------------------------------------------------------
/static/css/style.css:
--------------------------------------------------------------------------------
1 | /* 全局样式 */
2 | :root {
3 | --primary-color: #4285f4;
4 | --secondary-color: #34a853;
5 | --accent-color: #ea4335;
6 | --neutral-color: #fbbc05;
7 | --dark-color: #202124;
8 | --light-color: #f8f9fa;
9 | --gradient-1: linear-gradient(135deg, #4285f4, #34a853);
10 | --gradient-2: linear-gradient(135deg, #ea4335, #fbbc05);
11 | --gradient-3: linear-gradient(135deg, #34a853, #4285f4);
12 | --gradient-4: linear-gradient(135deg, #fbbc05, #ea4335);
13 | --gradient-5: linear-gradient(135deg, #4285f4, #ea4335);
14 | --gradient-6: linear-gradient(135deg, #34a853, #fbbc05);
15 | --box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);
16 | --transition: all 0.3s ease;
17 | }
18 |
19 | body {
20 | background-color: #ffffff;
21 | color: var(--dark-color);
22 | font-family: 'Poppins', sans-serif;
23 | overflow-x: hidden;
24 | }
25 |
26 | .container {
27 | max-width: 1200px;
28 | }
29 |
30 | /* 文字渐变效果 */
31 | .text-gradient {
32 | background: var(--gradient-1);
33 | -webkit-background-clip: text;
34 | background-clip: text;
35 | color: transparent;
36 | display: inline-block;
37 | }
38 |
39 | /* 英雄区域 */
40 | .hero-section {
41 | background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
42 | padding: 60px 0;
43 | margin-bottom: 50px;
44 | position: relative;
45 | overflow: hidden;
46 | }
47 |
48 | /* 设置按钮 */
49 | .settings-btn {
50 | background-color: var(--accent-color);
51 | border: none;
52 | color: white;
53 | font-weight: 600;
54 | padding: 0.6rem 1.2rem;
55 | border-radius: 50px;
56 | box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
57 | transition: all 0.3s ease;
58 | font-size: 1rem;
59 | letter-spacing: 0.5px;
60 | position: relative;
61 | z-index: 100;
62 | animation: pulse 2s infinite;
63 | }
64 |
65 | .settings-btn:hover {
66 | background-color: var(--neutral-color);
67 | color: var(--dark-color);
68 | transform: translateY(-2px);
69 | box-shadow: 0 6px 12px rgba(0, 0, 0, 0.15);
70 | animation: none;
71 | }
72 |
73 | @keyframes pulse {
74 | 0% {
75 | box-shadow: 0 0 0 0 rgba(234, 67, 53, 0.4);
76 | }
77 | 70% {
78 | box-shadow: 0 0 0 10px rgba(234, 67, 53, 0);
79 | }
80 | 100% {
81 | box-shadow: 0 0 0 0 rgba(234, 67, 53, 0);
82 | }
83 | }
84 |
85 | .hero-section::before {
86 | content: '';
87 | position: absolute;
88 | top: -50%;
89 | right: -50%;
90 | width: 100%;
91 | height: 100%;
92 | background: radial-gradient(circle, rgba(66, 133, 244, 0.1) 0%, rgba(66, 133, 244, 0) 70%);
93 | z-index: 0;
94 | }
95 |
96 | .hero-section::after {
97 | content: '';
98 | position: absolute;
99 | bottom: -50%;
100 | left: -50%;
101 | width: 100%;
102 | height: 100%;
103 | background: radial-gradient(circle, rgba(52, 168, 83, 0.1) 0%, rgba(52, 168, 83, 0) 70%);
104 | z-index: 0;
105 | }
106 |
107 | .hero-section .container {
108 | position: relative;
109 | z-index: 1;
110 | }
111 |
112 | /* 主要内容区域 */
113 | .main-content {
114 | position: relative;
115 | z-index: 1;
116 | padding-bottom: 50px;
117 | }
118 |
119 | /* 功能卡片样式 */
120 | .feature-card {
121 | background-color: #ffffff;
122 | border-radius: 16px;
123 | box-shadow: var(--box-shadow);
124 | padding: 30px;
125 | transition: var(--transition);
126 | height: 100%;
127 | display: flex;
128 | flex-direction: column;
129 | position: relative;
130 | overflow: hidden;
131 | border: 1px solid rgba(0,0,0,0.05);
132 | }
133 |
134 | .feature-card:hover {
135 | transform: translateY(-10px);
136 | box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15);
137 | }
138 |
139 | .feature-card::before {
140 | content: '';
141 | position: absolute;
142 | top: 0;
143 | left: 0;
144 | width: 100%;
145 | height: 5px;
146 | background: var(--gradient-1);
147 | opacity: 0;
148 | transition: var(--transition);
149 | }
150 |
151 | .feature-card:hover::before {
152 | opacity: 1;
153 | }
154 |
155 | .icon-wrapper {
156 | width: 60px;
157 | height: 60px;
158 | border-radius: 50%;
159 | display: flex;
160 | align-items: center;
161 | justify-content: center;
162 | margin-bottom: 20px;
163 | color: white;
164 | font-size: 24px;
165 | }
166 |
167 | .bg-gradient-1 { background: var(--gradient-1); }
168 | .bg-gradient-2 { background: var(--gradient-2); }
169 | .bg-gradient-3 { background: var(--gradient-3); }
170 | .bg-gradient-4 { background: var(--gradient-4); }
171 | .bg-gradient-5 { background: var(--gradient-5); }
172 | .bg-gradient-6 { background: var(--gradient-6); }
173 |
174 | .feature-title {
175 | font-size: 1.5rem;
176 | font-weight: 600;
177 | margin-bottom: 15px;
178 | color: var(--dark-color);
179 | }
180 |
181 | .feature-text {
182 | color: #5f6368;
183 | margin-bottom: 25px;
184 | flex-grow: 1;
185 | }
186 |
187 | .coming-soon {
188 | position: relative;
189 | opacity: 0.8;
190 | }
191 |
192 | .coming-soon-badge {
193 | position: absolute;
194 | top: 20px;
195 | right: 20px;
196 | padding: 8px 12px;
197 | border-radius: 30px;
198 | font-size: 0.75rem;
199 | font-weight: 600;
200 | color: white;
201 | }
202 |
203 | /* 标题样式 */
204 | .section-title {
205 | font-weight: 700;
206 | margin-bottom: 40px;
207 | position: relative;
208 | display: inline-block;
209 | padding-bottom: 10px;
210 | }
211 |
212 | .section-title::after {
213 | content: '';
214 | position: absolute;
215 | bottom: 0;
216 | left: 50%;
217 | transform: translateX(-50%);
218 | width: 80px;
219 | height: 4px;
220 | background: var(--gradient-1);
221 | border-radius: 2px;
222 | }
223 |
224 | /* 卡片样式 */
225 | .card {
226 | border-radius: 16px;
227 | box-shadow: var(--box-shadow);
228 | transition: var(--transition);
229 | border: none;
230 | overflow: hidden;
231 | }
232 |
233 | .card:hover {
234 | transform: translateY(-10px);
235 | box-shadow: 0 15px 30px rgba(0, 0, 0, 0.15);
236 | }
237 |
238 | .card-header {
239 | background-color: #f1f8ff;
240 | border-bottom: 1px solid #e3f2fd;
241 | padding: 15px 20px;
242 | }
243 |
244 | /* 按钮样式 */
245 | .btn-gradient {
246 | background: var(--gradient-1);
247 | border: none;
248 | color: white;
249 | padding: 10px 25px;
250 | border-radius: 30px;
251 | font-weight: 500;
252 | transition: var(--transition);
253 | box-shadow: 0 4px 10px rgba(66, 133, 244, 0.3);
254 | }
255 |
256 | .btn-gradient:hover {
257 | transform: translateY(-3px);
258 | box-shadow: 0 8px 15px rgba(66, 133, 244, 0.4);
259 | color: white;
260 | }
261 |
262 | .btn-outline-gradient {
263 | background: white;
264 | color: var(--primary-color);
265 | border: 2px solid transparent;
266 | background-image: linear-gradient(white, white), var(--gradient-1);
267 | background-origin: border-box;
268 | background-clip: padding-box, border-box;
269 | padding: 8px 20px;
270 | border-radius: 30px;
271 | font-weight: 500;
272 | transition: var(--transition);
273 | display: inline-flex;
274 | align-items: center;
275 | justify-content: center;
276 | gap: 8px;
277 | }
278 |
279 | .btn-outline-gradient span {
280 | transition: var(--transition);
281 | }
282 |
283 | .btn-outline-gradient i {
284 | transition: var(--transition);
285 | }
286 |
287 | .btn-outline-gradient:hover {
288 | box-shadow: 0 5px 15px rgba(66, 133, 244, 0.2);
289 | color: var(--primary-color);
290 | }
291 |
292 | .btn-outline-gradient:hover i {
293 | transform: translateX(5px);
294 | }
295 |
296 | .btn-primary {
297 | background-color: var(--primary-color);
298 | border-color: var(--primary-color);
299 | border-radius: 30px;
300 | padding: 8px 20px;
301 | font-weight: 500;
302 | transition: var(--transition);
303 | }
304 |
305 | .btn-primary:hover {
306 | background-color: #3367d6;
307 | border-color: #3367d6;
308 | transform: translateY(-3px);
309 | box-shadow: 0 5px 15px rgba(66, 133, 244, 0.3);
310 | }
311 |
312 | .btn-outline-secondary {
313 | color: #5f6368;
314 | border-color: #dadce0;
315 | border-radius: 30px;
316 | padding: 8px 20px;
317 | font-weight: 500;
318 | transition: var(--transition);
319 | }
320 |
321 | .btn-outline-secondary:hover {
322 | background-color: #f1f3f4;
323 | color: #202124;
324 | transform: translateY(-3px);
325 | }
326 |
327 | /* 表单样式 */
328 | .form-control:focus {
329 | border-color: #4285f4;
330 | box-shadow: 0 0 0 0.25rem rgba(66, 133, 244, 0.25);
331 | }
332 |
333 | /* 结果展示区域 */
334 | .answer-box, .description-box {
335 | background-color: #f8f9fa;
336 | border-radius: 8px;
337 | padding: 15px;
338 | border: 1px solid #e9ecef;
339 | max-height: 300px;
340 | overflow-y: auto;
341 | }
342 |
343 | .raw-response {
344 | background-color: #f8f9fa;
345 | border-radius: 8px;
346 | padding: 15px;
347 | border: 1px solid #e9ecef;
348 | font-size: 0.9rem;
349 | max-height: 200px;
350 | overflow-y: auto;
351 | white-space: pre-wrap;
352 | }
353 |
354 | .bbox-coordinates {
355 | font-family: monospace;
356 | background-color: #f8f9fa;
357 | border-radius: 8px;
358 | padding: 15px;
359 | border: 1px solid #e9ecef;
360 | }
361 |
362 | /* 响应式图片 */
363 | .img-fluid {
364 | max-height: 300px;
365 | width: auto;
366 | margin: 0 auto;
367 | display: block;
368 | }
369 |
370 | /* 图片容器相对定位 */
371 | .position-relative {
372 | position: relative;
373 | overflow: hidden;
374 | }
375 |
376 | /* 下载按钮样式 */
377 | .position-relative .btn-success {
378 | opacity: 0.7;
379 | transition: opacity 0.3s ease;
380 | }
381 |
382 | .position-relative:hover .btn-success {
383 | opacity: 1;
384 | }
385 |
386 | /* 复制按钮样式 */
387 | .copy-btn {
388 | transition: all 0.3s ease;
389 | }
390 |
391 | .copy-btn:hover {
392 | transform: translateY(-2px);
393 | box-shadow: 0 2px 5px rgba(0,0,0,0.1);
394 | }
395 |
396 | /* 页脚样式 */
397 | .footer-section {
398 | background-color: #f8f9fa;
399 | border-top: 1px solid #e9ecef;
400 | color: #5f6368;
401 | font-size: 0.9rem;
402 | }
403 |
404 | .footer-link {
405 | color: var(--primary-color);
406 | text-decoration: none;
407 | font-weight: 500;
408 | transition: var(--transition);
409 | }
410 |
411 | .footer-link:hover {
412 | color: var(--secondary-color);
413 | text-decoration: underline;
414 | }
415 |
416 | .social-links {
417 | display: flex;
418 | gap: 15px;
419 | justify-content: center;
420 | }
421 |
422 | .social-link {
423 | display: flex;
424 | align-items: center;
425 | justify-content: center;
426 | width: 36px;
427 | height: 36px;
428 | border-radius: 50%;
429 | background-color: #e9ecef;
430 | color: #5f6368;
431 | font-size: 18px;
432 | transition: var(--transition);
433 | }
434 |
435 | .social-link:hover {
436 | background-color: var(--primary-color);
437 | color: white;
438 | transform: translateY(-3px);
439 | }
440 |
441 | footer {
442 | color: #5f6368;
443 | font-size: 0.9rem;
444 | }
445 |
446 | footer a {
447 | color: #4285f4;
448 | text-decoration: none;
449 | }
450 |
451 | footer a:hover {
452 | text-decoration: underline;
453 | }
454 |
455 | /* 加载动画 */
456 | .loading {
457 | display: none;
458 | text-align: center;
459 | padding: 20px;
460 | }
461 |
462 | .loading-spinner {
463 | width: 40px;
464 | height: 40px;
465 | margin: 0 auto;
466 | border: 4px solid #f3f3f3;
467 | border-top: 4px solid #4285f4;
468 | border-radius: 50%;
469 | animation: spin 2s linear infinite;
470 | }
471 |
472 | @keyframes spin {
473 | 0% { transform: rotate(0deg); }
474 | 100% { transform: rotate(360deg); }
475 | }
476 |
477 | /* 浮动动画 */
478 | @keyframes float {
479 | 0% {
480 | transform: translateY(0px);
481 | }
482 | 50% {
483 | transform: translateY(-10px);
484 | }
485 | 100% {
486 | transform: translateY(0px);
487 | }
488 | }
489 |
490 | /* 脉冲动画 */
491 | @keyframes pulse {
492 | 0% {
493 | box-shadow: 0 0 0 0 rgba(66, 133, 244, 0.4);
494 | }
495 | 70% {
496 | box-shadow: 0 0 0 10px rgba(66, 133, 244, 0);
497 | }
498 | 100% {
499 | box-shadow: 0 0 0 0 rgba(66, 133, 244, 0);
500 | }
501 | }
502 |
--------------------------------------------------------------------------------
/templates/settings.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | API密钥设置 - Gemini 图像应用
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
45 |
46 |
47 |
48 |
55 |
56 |
57 |
58 |
59 |
62 |
63 |
64 |
65 |
66 |
当前API密钥
67 |
68 |
69 | {% if has_key %}
70 |
71 |
72 |
73 |
74 |
75 |
已设置API密钥
76 |
出于安全考虑,密钥不会显示
77 |
78 |
79 | {% else %}
80 |
81 |
82 |
83 |
84 |
未设置API密钥
85 |
86 | {% endif %}
87 |
88 | {% if has_key %}
89 |
92 |
95 | {% endif %}
96 |
97 |
98 |
99 |
100 |
设置新的API密钥
101 |
113 |
114 |
115 |
116 |
帮助信息
117 |
118 |
如何获取API密钥?
119 |
120 | - 访问 Google AI Studio
121 | - 登录您的Google账户
122 | - 点击"创建API密钥"按钮
123 | - 复制生成的API密钥
124 | - 粘贴到上面的输入框中并保存
125 |
126 |
127 | 请妥善保管您的API密钥,不要分享给他人
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
249 |
250 |
251 |
--------------------------------------------------------------------------------
/app.py:
--------------------------------------------------------------------------------
1 | import os
2 | import base64
3 | import io
4 | import json
5 | import logging
6 | from PIL import Image, ImageDraw
7 | from flask import Flask, render_template, request, jsonify, redirect, url_for
8 | from dotenv import load_dotenv
9 | from google import genai
10 | from google.genai import types
11 |
12 | # 配置日志
13 | logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
14 |
15 | # 加载环境变量
16 | load_dotenv()
17 |
18 | # 获取Gemini API密钥
19 | api_key = os.getenv("GEMINI_API_KEY")
20 | if not api_key:
21 | logging.error("Gemini API密钥未设置,请在.env文件中设置GEMINI_API_KEY")
22 | else:
23 | logging.info("Gemini API密钥已设置")
24 |
25 | # 创建一个文件来存储API密钥
26 | API_KEY_FILE = 'api_key.json'
27 |
28 | # 如果文件不存在,则创建并存储环境变量中的API密钥
29 | if not os.path.exists(API_KEY_FILE):
30 | with open(API_KEY_FILE, 'w') as f:
31 | json.dump({'api_key': api_key}, f)
32 |
33 | # 从文件中读取API密钥
34 | def get_api_key():
35 | try:
36 | with open(API_KEY_FILE, 'r') as f:
37 | data = json.load(f)
38 | return data.get('api_key')
39 | except (FileNotFoundError, json.JSONDecodeError):
40 | return api_key
41 |
42 | # 更新API密钥
43 | def update_api_key(new_api_key):
44 | with open(API_KEY_FILE, 'w') as f:
45 | json.dump({'api_key': new_api_key}, f)
46 | global api_key
47 | api_key = new_api_key
48 |
49 | app = Flask(__name__)
50 | app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 限制上传文件大小为16MB
51 |
52 | # 创建上传目录
53 | UPLOAD_FOLDER = 'static/uploads'
54 | if not os.path.exists(UPLOAD_FOLDER):
55 | os.makedirs(UPLOAD_FOLDER)
56 | app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
57 |
58 | # 创建生成图像目录
59 | GENERATED_FOLDER = 'static/generated'
60 | if not os.path.exists(GENERATED_FOLDER):
61 | os.makedirs(GENERATED_FOLDER)
62 | app.config['GENERATED_FOLDER'] = GENERATED_FOLDER
63 |
64 | # 主页
65 | @app.route('/')
66 | def index():
67 | return render_template('index.html')
68 |
69 | # API密钥设置页面
70 | @app.route('/settings')
71 | def settings():
72 | current_api_key = get_api_key()
73 | # 完全隐藏密钥,只显示星号
74 | has_key = bool(current_api_key)
75 | masked_key = '*' * 20 if has_key else ''
76 | return render_template('settings.html', masked_key=masked_key, has_key=has_key)
77 |
78 | # 更新API密钥
79 | @app.route('/update_api_key', methods=['POST'])
80 | def update_api_key_route():
81 | new_api_key = request.form.get('api_key')
82 | if not new_api_key:
83 | return jsonify({
84 | 'status': 'error',
85 | 'message': 'API密钥不能为空'
86 | })
87 |
88 | update_api_key(new_api_key)
89 | logging.info("API密钥已更新")
90 |
91 | return jsonify({
92 | 'status': 'success',
93 | 'message': 'API密钥已成功更新'
94 | })
95 |
96 | # 删除API密钥
97 | @app.route('/delete_api_key', methods=['POST'])
98 | def delete_api_key():
99 | update_api_key('')
100 | logging.info("API密钥已删除")
101 |
102 | return jsonify({
103 | 'status': 'success',
104 | 'message': 'API密钥已成功删除'
105 | })
106 |
107 | # API密钥测试
108 | @app.route('/test_api')
109 | def test_api():
110 | current_api_key = get_api_key()
111 | if not current_api_key:
112 | return jsonify({
113 | 'status': 'error',
114 | 'message': 'API密钥未设置',
115 | 'error_type': 'missing'
116 | })
117 |
118 | try:
119 | client = genai.Client(api_key=current_api_key)
120 | logging.info("测试API密钥: 成功创建Gemini客户端")
121 |
122 | # 发送简单的文本请求来测试API密钥
123 | response = client.models.generate_content(
124 | model="gemini-2.0-flash",
125 | contents="Hello, this is a test."
126 | )
127 | logging.info("测试API密钥: 成功收到响应")
128 |
129 | return jsonify({
130 | 'status': 'success',
131 | 'message': '您的API密钥有效并可以正常使用',
132 | 'response': response.text
133 | })
134 | except genai.types.api_error.ApiError as api_err:
135 | error_msg = str(api_err)
136 | logging.error(f"测试API密钥错误: {error_msg}")
137 | return jsonify({
138 | 'status': 'error',
139 | 'message': f'API错误: {error_msg}',
140 | 'error_type': '403' if '403' in error_msg else 'other'
141 | }), 400
142 | except Exception as e:
143 | logging.error(f"测试API密钥时发生未知错误: {str(e)}")
144 | return jsonify({
145 | 'status': 'error',
146 | 'message': f'未知错误: {str(e)}'
147 | }), 500
148 |
149 | # 图像问答
150 | @app.route('/image_qa', methods=['GET', 'POST'])
151 | def image_qa():
152 | if request.method == 'POST':
153 | # 检查是否有文件上传
154 | if 'image' not in request.files:
155 | return render_template('image_qa.html', error='没有选择图片')
156 |
157 | file = request.files['image']
158 | if file.filename == '':
159 | return render_template('image_qa.html', error='没有选择图片')
160 |
161 | # 保存上传的图片
162 | image_path = os.path.join(app.config['UPLOAD_FOLDER'], file.filename)
163 | file.save(image_path)
164 |
165 | # 确保图片路径是相对于static目录的
166 | relative_image_path = image_path.replace('static/', '')
167 | logging.info(f"保存图片到: {image_path}, 相对路径: {relative_image_path}")
168 |
169 | # 获取问题
170 | question = request.form.get('question', '这张图片里有什么?')
171 |
172 | try:
173 | # 加载图片
174 | image = Image.open(image_path)
175 | logging.info(f"成功加载图片: {image_path}")
176 |
177 | # 获取API密钥
178 | current_api_key = get_api_key()
179 | if not current_api_key:
180 | return render_template('image_qa.html', error='API密钥未设置,请先设置API密钥')
181 |
182 | # 创建Gemini客户端
183 | client = genai.Client(api_key=current_api_key)
184 | logging.info("成功创建Gemini客户端")
185 |
186 | try:
187 | # 调用Gemini API进行图像问答
188 | logging.info(f"发送请求到Gemini API, 模型: gemini-2.0-flash, 问题: {question}")
189 | response = client.models.generate_content(
190 | model="gemini-2.0-flash",
191 | contents=[question, image]
192 | )
193 | logging.info("成功收到Gemini API响应")
194 |
195 | return render_template('image_qa.html',
196 | image_path=relative_image_path,
197 | question=question,
198 | answer=response.text)
199 | except genai.types.api_error.ApiError as api_err:
200 | error_msg = str(api_err)
201 | logging.error(f"Gemini API错误: {error_msg}")
202 | if '403' in error_msg:
203 | return render_template('image_qa.html',
204 | image_path=relative_image_path,
205 | question=question,
206 | error=f'API权限错误(403): 请检查您的API密钥是否有效或权限是否足够')
207 | else:
208 | return render_template('image_qa.html',
209 | image_path=relative_image_path,
210 | question=question,
211 | error=f'Gemini API错误: {error_msg}')
212 | except Exception as e:
213 | logging.error(f"处理图片时出错: {str(e)}")
214 | return render_template('image_qa.html', error=f'处理图片时出错: {str(e)}')
215 |
216 | return render_template('image_qa.html')
217 |
218 | # 图像生成
219 | @app.route('/image_generation', methods=['GET', 'POST'])
220 | def image_generation():
221 | if request.method == 'POST':
222 | prompt = request.form.get('prompt', '')
223 | model_type = request.form.get('model_type', 'gemini')
224 |
225 | if not prompt:
226 | return render_template('image_generation.html', error='请输入提示词')
227 |
228 | try:
229 | # 获取API密钥
230 | current_api_key = get_api_key()
231 | if not current_api_key:
232 | return render_template('image_generation.html', error='API密钥未设置,请先设置API密钥')
233 |
234 | client = genai.Client(api_key=current_api_key)
235 |
236 | if model_type == 'gemini':
237 | # 使用Gemini生成图像
238 | response = client.models.generate_content(
239 | model="gemini-2.0-flash-exp-image-generation",
240 | contents=prompt,
241 | config=types.GenerateContentConfig(
242 | response_modalities=['Text', 'Image']
243 | )
244 | )
245 |
246 | # 保存生成的图像
247 | image_path = None
248 | description = None
249 |
250 | for part in response.candidates[0].content.parts:
251 | if part.text is not None:
252 | description = part.text
253 | elif part.inline_data is not None:
254 | # 保存图像
255 | image = Image.open(io.BytesIO(part.inline_data.data))
256 | image_filename = f"gemini_{base64.b64encode(prompt.encode()).decode()[:10]}.png"
257 | image_path = os.path.join(app.config['GENERATED_FOLDER'], image_filename)
258 | image.save(image_path)
259 | image_path = image_path.replace('static/', '')
260 |
261 | return render_template('image_generation.html',
262 | prompt=prompt,
263 | image_path=image_path,
264 | description=description,
265 | model_type=model_type)
266 | else:
267 | # 使用Imagen生成图像
268 | response = client.models.generate_images(
269 | model='imagen-3.0-generate-002',
270 | prompt=prompt,
271 | config=types.GenerateImagesConfig(
272 | number_of_images=1,
273 | )
274 | )
275 |
276 | # 保存生成的图像
277 | image = Image.open(io.BytesIO(response.generated_images[0].image.image_bytes))
278 | image_filename = f"imagen_{base64.b64encode(prompt.encode()).decode()[:10]}.png"
279 | image_path = os.path.join(app.config['GENERATED_FOLDER'], image_filename)
280 | image.save(image_path)
281 | image_path = image_path.replace('static/', '')
282 |
283 | return render_template('image_generation.html',
284 | prompt=prompt,
285 | image_path=image_path,
286 | model_type=model_type)
287 |
288 | except Exception as e:
289 | return render_template('image_generation.html', error=f'生成图像时出错: {str(e)}')
290 |
291 | return render_template('image_generation.html')
292 |
293 | # 图像编辑
294 | @app.route('/image_editing', methods=['GET', 'POST'])
295 | def image_editing():
296 | if request.method == 'POST':
297 | # 检查是否有文件上传
298 | if 'image' not in request.files:
299 | return render_template('image_editing.html', error='没有选择图片')
300 |
301 | file = request.files['image']
302 | if file.filename == '':
303 | return render_template('image_editing.html', error='没有选择图片')
304 |
305 | # 保存上传的图片
306 | image_path = os.path.join(app.config['UPLOAD_FOLDER'], file.filename)
307 | file.save(image_path)
308 |
309 | # 获取编辑指令
310 | edit_prompt = request.form.get('edit_prompt', '给这张图片添加一些效果')
311 |
312 | try:
313 | # 加载图片
314 | image = Image.open(image_path)
315 |
316 | # 获取API密钥
317 | current_api_key = get_api_key()
318 | if not current_api_key:
319 | return render_template('image_editing.html', error='API密钥未设置,请先设置API密钥')
320 |
321 | # 创建Gemini客户端
322 | client = genai.Client(api_key=current_api_key)
323 |
324 | # 调用Gemini API进行图像编辑
325 | response = client.models.generate_content(
326 | model="gemini-2.0-flash-exp-image-generation",
327 | contents=[edit_prompt, image],
328 | config=types.GenerateContentConfig(
329 | response_modalities=['Text', 'Image']
330 | )
331 | )
332 |
333 | # 保存编辑后的图像
334 | edited_image_path = None
335 | description = None
336 |
337 | for part in response.candidates[0].content.parts:
338 | if part.text is not None:
339 | description = part.text
340 | elif part.inline_data is not None:
341 | # 保存图像
342 | edited_image = Image.open(io.BytesIO(part.inline_data.data))
343 | edited_filename = f"edited_{os.path.basename(image_path)}"
344 | edited_image_path = os.path.join(app.config['GENERATED_FOLDER'], edited_filename)
345 | edited_image.save(edited_image_path)
346 | edited_image_path = edited_image_path.replace('static/', '')
347 |
348 | return render_template('image_editing.html',
349 | original_image=image_path.replace('static/', ''),
350 | edited_image=edited_image_path,
351 | edit_prompt=edit_prompt,
352 | description=description)
353 | except Exception as e:
354 | return render_template('image_editing.html', error=f'编辑图片时出错: {str(e)}')
355 |
356 | return render_template('image_editing.html')
357 |
358 | # 边界框检测
359 | @app.route('/bounding_box', methods=['GET', 'POST'])
360 | def bounding_box():
361 | if request.method == 'POST':
362 | # 检查是否有文件上传
363 | if 'image' not in request.files:
364 | return render_template('bounding_box.html', error='没有选择图片')
365 |
366 | file = request.files['image']
367 | if file.filename == '':
368 | return render_template('bounding_box.html', error='没有选择图片')
369 |
370 | # 保存上传的图片
371 | image_path = os.path.join(app.config['UPLOAD_FOLDER'], file.filename)
372 | file.save(image_path)
373 |
374 | # 获取检测对象
375 | detect_object = request.form.get('detect_object', '检测图片中的所有对象')
376 |
377 | try:
378 | # 加载图片
379 | image = Image.open(image_path)
380 | width, height = image.size
381 |
382 | # 获取API密钥
383 | current_api_key = get_api_key()
384 | if not current_api_key:
385 | return render_template('bounding_box.html', error='API密钥未设置,请先设置API密钥')
386 |
387 | # 创建Gemini客户端
388 | client = genai.Client(api_key=current_api_key)
389 |
390 | # 构建提示词
391 | prompt = f"返回图片中{detect_object}的边界框坐标,格式为[ymin, xmin, ymax, xmax]。坐标值应该在0到1000之间。"
392 |
393 | # 调用Gemini API进行边界框检测
394 | response = client.models.generate_content(
395 | model="gemini-1.5-pro",
396 | contents=[image, prompt]
397 | )
398 |
399 | # 解析边界框坐标
400 | bbox_text = response.text.strip()
401 |
402 | # 尝试提取坐标
403 | import re
404 | coords = re.findall(r'\[(\d+),\s*(\d+),\s*(\d+),\s*(\d+)\]', bbox_text)
405 |
406 | if not coords:
407 | return render_template('bounding_box.html',
408 | image_path=image_path.replace('static/', ''),
409 | detect_object=detect_object,
410 | error=f'无法解析边界框坐标: {bbox_text}')
411 |
412 | # 创建带边界框的图像
413 | draw = ImageDraw.Draw(image)
414 |
415 | bboxes = []
416 | for coord in coords:
417 | y_min, x_min, y_max, x_max = map(int, coord)
418 |
419 | # 归一化坐标
420 | x_min_norm = (x_min / 1000) * width
421 | y_min_norm = (y_min / 1000) * height
422 | x_max_norm = (x_max / 1000) * width
423 | y_max_norm = (y_max / 1000) * height
424 |
425 | # 绘制边界框
426 | draw.rectangle([(x_min_norm, y_min_norm), (x_max_norm, y_max_norm)],
427 | outline="red", width=3)
428 |
429 | bboxes.append({
430 | 'coordinates': [y_min, x_min, y_max, x_max],
431 | 'normalized': [y_min_norm, x_min_norm, y_max_norm, x_max_norm]
432 | })
433 |
434 | # 保存带边界框的图像
435 | bbox_filename = f"bbox_{os.path.basename(image_path)}"
436 | bbox_image_path = os.path.join(app.config['GENERATED_FOLDER'], bbox_filename)
437 | image.save(bbox_image_path)
438 |
439 | return render_template('bounding_box.html',
440 | image_path=image_path.replace('static/', ''),
441 | bbox_image=bbox_image_path.replace('static/', ''),
442 | detect_object=detect_object,
443 | bboxes=bboxes,
444 | raw_response=bbox_text)
445 | except Exception as e:
446 | return render_template('bounding_box.html', error=f'检测边界框时出错: {str(e)}')
447 |
448 | return render_template('bounding_box.html')
449 |
450 | # 图像分割
451 | @app.route('/image_segmentation', methods=['GET', 'POST'])
452 | def image_segmentation():
453 | if request.method == 'POST':
454 | # 检查是否有文件上传
455 | if 'image' not in request.files:
456 | return render_template('image_segmentation.html', error='没有选择图片')
457 |
458 | file = request.files['image']
459 | if file.filename == '':
460 | return render_template('image_segmentation.html', error='没有选择图片')
461 |
462 | # 保存上传的图片
463 | image_path = os.path.join(app.config['UPLOAD_FOLDER'], file.filename)
464 | file.save(image_path)
465 |
466 | # 获取分割对象
467 | segment_object = request.form.get('segment_object', '分割图片中的所有对象')
468 |
469 | try:
470 | # 加载图片
471 | image = Image.open(image_path).convert("RGBA")
472 |
473 | # 获取API密钥
474 | current_api_key = get_api_key()
475 | if not current_api_key:
476 | return render_template('image_segmentation.html', error='API密钥未设置,请先设置API密钥')
477 |
478 | # 创建Gemini客户端
479 | client = genai.Client(api_key=current_api_key)
480 |
481 | # 构建提示词
482 | prompt = f"""
483 | 给出图片中{segment_object}的分割掩码。
484 | 输出一个JSON列表,每个条目包含2D边界框(键为"box_2d"),分割掩码(键为"mask"),
485 | 以及文本标签(键为"label")。使用描述性标签。
486 | """
487 |
488 | # 调用Gemini API进行图像分割
489 | response = client.models.generate_content(
490 | model="gemini-2.5-pro-exp-03-25",
491 | contents=[image, prompt]
492 | )
493 |
494 | # 解析JSON响应
495 | response_text = response.text
496 |
497 | # 提取JSON部分
498 | if "```json" in response_text:
499 | json_str = response_text.split("```json")[1].split("```")[0].strip()
500 | elif "[" in response_text and "]" in response_text:
501 | start = response_text.find("[")
502 | end = response_text.rfind("]") + 1
503 | json_str = response_text[start:end]
504 | else:
505 | json_str = response_text
506 |
507 | try:
508 | mask_data = json.loads(json_str)
509 | except:
510 | return render_template('image_segmentation.html',
511 | image_path=image_path.replace('static/', ''),
512 | segment_object=segment_object,
513 | error=f'无法解析JSON响应: {response_text}')
514 |
515 | if not mask_data:
516 | return render_template('image_segmentation.html',
517 | image_path=image_path.replace('static/', ''),
518 | segment_object=segment_object,
519 | error='未找到分割掩码')
520 |
521 | # 处理第一个掩码
522 | first_mask = mask_data[0]
523 |
524 | # 提取base64编码的掩码
525 | mask_base64 = first_mask.get("mask", "")
526 | if "base64," in mask_base64:
527 | mask_base64 = mask_base64.split("base64,")[1]
528 |
529 | # 解码并加载掩码图像
530 | mask_bytes = base64.b64decode(mask_base64)
531 | mask_image = Image.open(io.BytesIO(mask_bytes))
532 |
533 | # 转换图像为RGBA
534 | mask_image = mask_image.convert("L") # 转换掩码为灰度
535 |
536 | # 创建一个彩色覆盖层(亮粉色)
537 | overlay = Image.new("RGBA", mask_image.size, (255, 0, 255, 128)) # 亮粉色,半透明
538 |
539 | # 使用掩码确定应用颜色的位置
540 | overlay.putalpha(mask_image)
541 |
542 | # 调整覆盖层大小以匹配原始图像(如果需要)
543 | if overlay.size != image.size:
544 | overlay = overlay.resize(image.size)
545 |
546 | # 将彩色掩码覆盖在原始图像上
547 | result = Image.alpha_composite(image, overlay)
548 |
549 | # 保存掩码图像
550 | mask_filename = f"mask_{os.path.basename(image_path)}"
551 | mask_image_path = os.path.join(app.config['GENERATED_FOLDER'], mask_filename)
552 | mask_image.save(mask_image_path)
553 |
554 | # 保存合并后的图像
555 | merged_filename = f"segmented_{os.path.basename(image_path)}"
556 | merged_image_path = os.path.join(app.config['GENERATED_FOLDER'], merged_filename)
557 | result.save(merged_image_path)
558 |
559 | return render_template('image_segmentation.html',
560 | image_path=image_path.replace('static/', ''),
561 | mask_image=mask_image_path.replace('static/', ''),
562 | segmented_image=merged_image_path.replace('static/', ''),
563 | segment_object=segment_object,
564 | raw_response=response_text)
565 | except Exception as e:
566 | return render_template('image_segmentation.html', error=f'图像分割时出错: {str(e)}')
567 |
568 | return render_template('image_segmentation.html')
569 |
570 | if __name__ == '__main__':
571 | app.run(debug=True)
572 |
--------------------------------------------------------------------------------