Ostrakon-VL-8B快速原型:10分钟用Python Flask搭建模型演示Web界面

发布时间:2026/6/21 20:01:32

Ostrakon-VL-8B快速原型:10分钟用Python Flask搭建模型演示Web界面 Ostrakon-VL-8B快速原型10分钟用Python Flask搭建模型演示Web界面你是不是刚拿到一个像Ostrakon-VL-8B这样的视觉语言模型想快速做个演示界面给同事或客户看看效果或者你有个技术分享会需要现场展示模型能力但不想花几天时间去折腾复杂的Web开发今天我就带你用Python的Flask框架在10分钟左右的时间里快速搭建一个能上传图片、提问、并展示模型回答的Web演示界面。整个过程就像搭积木一样简单不需要前端框架不需要复杂配置跟着步骤走就行。1. 环境准备安装必要的工具在开始之前我们先确保电脑上已经装好了Python。打开你的命令行工具Windows上是命令提示符或PowerShellMac或Linux上是终端输入以下命令检查一下python --version如果显示的是Python 3.7或更高版本那就没问题。如果没有安装Python可以去Python官网下载安装这里就不展开说了。接下来我们需要安装几个Python库。创建一个新的文件夹比如叫ostrakon_demo然后在命令行里进入这个文件夹执行下面的安装命令pip install flask pillow requests简单解释一下这几个库是干什么的Flask一个非常轻量级的Web框架用来搭建我们的网站后端。PillowPython里处理图片的库我们会用它来检查上传的图片。requests用来发送HTTP请求如果你的模型是通过API调用的就会用到它。安装过程很快通常一两分钟就完成了。2. 创建Flask应用的核心文件现在我们来创建第一个文件这是整个Web应用的核心。在ostrakon_demo文件夹里新建一个文件命名为app.py。打开这个文件把下面的代码复制进去from flask import Flask, render_template, request, jsonify import os from werkzeug.utils import secure_filename from PIL import Image import requests import json app Flask(__name__) # 设置一些基本配置 app.config[UPLOAD_FOLDER] static/uploads # 上传图片保存的文件夹 app.config[MAX_CONTENT_LENGTH] 5 * 1024 * 1024 # 限制上传文件最大5MB app.config[ALLOWED_EXTENSIONS] {png, jpg, jpeg, gif} # 允许的图片格式 # 创建上传文件夹如果不存在的话 os.makedirs(app.config[UPLOAD_FOLDER], exist_okTrue) def allowed_file(filename): 检查文件格式是否允许 return . in filename and \ filename.rsplit(., 1)[1].lower() in app.config[ALLOWED_EXTENSIONS] app.route(/) def index(): 首页显示上传界面 return render_template(index.html) app.route(/upload, methods[POST]) def upload_file(): 处理图片上传 if file not in request.files: return jsonify({error: 没有选择文件}), 400 file request.files[file] if file.filename : return jsonify({error: 没有选择文件}), 400 if file and allowed_file(file.filename): # 安全地处理文件名 filename secure_filename(file.filename) filepath os.path.join(app.config[UPLOAD_FOLDER], filename) file.save(filepath) # 返回上传成功的消息和文件路径 return jsonify({ success: True, filename: filename, filepath: f/static/uploads/{filename} }) return jsonify({error: 文件格式不支持请上传PNG、JPG、JPEG或GIF格式}), 400 app.route(/ask, methods[POST]) def ask_question(): 处理用户提问调用模型API data request.json image_path data.get(image_path, ) question data.get(question, ) if not image_path or not question: return jsonify({error: 请提供图片路径和问题}), 400 # 这里是你调用Ostrakon-VL-8B模型的地方 # 根据你的模型部署方式修改下面的代码 # 示例1如果模型部署在本地API # response call_local_model(image_path, question) # 示例2如果使用在线API比如OpenAI格式的接口 # api_response requests.post(http://你的模型地址/v1/chat/completions, # json{ # model: ostrakon-vl-8b, # messages: [ # {role: user, content: f图片信息: {image_path}, 问题: {question}} # ] # }) # response api_response.json() # 为了演示我们先返回一个模拟的响应 # 在实际使用时请替换为真实的模型调用代码 mock_response { answer: f根据图片内容分析{question}的答案是这是一张示例图片模型识别出了其中的主要物体和场景。在实际使用中这里会显示Ostrakon-VL-8B模型对图片的真实分析结果。 } return jsonify(mock_response) def call_local_model(image_path, question): 调用本地部署的模型的示例函数 # 这里写你实际调用模型的代码 # 比如使用transformers库加载模型并进行推理 pass if __name__ __main__: app.run(debugTrue, port5000)这段代码做了几件事创建了一个Flask应用设置了基本的配置。定义了三个主要的页面路由首页、上传图片、提问。包含了文件上传的检查逻辑。预留了调用模型API的位置你需要根据实际情况修改。3. 创建网页界面Flask默认会在templates文件夹里找HTML模板文件。所以我们在ostrakon_demo文件夹里创建一个templates文件夹然后在里面新建一个index.html文件。把下面的代码复制到index.html中!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleOstrakon-VL-8B 演示界面/title style * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif; line-height: 1.6; color: #333; max-width: 1200px; margin: 0 auto; padding: 20px; background-color: #f5f5f5; } .container { background: white; border-radius: 12px; padding: 30px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } header { text-align: center; margin-bottom: 40px; padding-bottom: 20px; border-bottom: 2px solid #eaeaea; } h1 { color: #2c3e50; margin-bottom: 10px; font-size: 2.5em; } .subtitle { color: #7f8c8d; font-size: 1.1em; } .main-content { display: grid; grid-template-columns: 1fr 1fr; gap: 30px; margin-bottom: 30px; } media (max-width: 768px) { .main-content { grid-template-columns: 1fr; } } .upload-section, .qa-section { background: #f8f9fa; padding: 25px; border-radius: 8px; border: 1px solid #eaeaea; } h2 { color: #3498db; margin-bottom: 20px; font-size: 1.5em; } .upload-area { border: 2px dashed #3498db; border-radius: 8px; padding: 40px 20px; text-align: center; margin-bottom: 20px; cursor: pointer; transition: all 0.3s ease; } .upload-area:hover { background: #f0f8ff; border-color: #2980b9; } .upload-area.dragover { background: #e3f2fd; border-color: #1a73e8; } #fileInput { display: none; } .upload-btn { background: #3498db; color: white; border: none; padding: 12px 24px; border-radius: 6px; cursor: pointer; font-size: 16px; transition: background 0.3s ease; } .upload-btn:hover { background: #2980b9; } .preview-container { margin-top: 20px; text-align: center; } #imagePreview { max-width: 100%; max-height: 300px; border-radius: 6px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); display: none; } .question-input { width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 16px; margin-bottom: 15px; resize: vertical; min-height: 100px; } .ask-btn { background: #2ecc71; color: white; border: none; padding: 12px 24px; border-radius: 6px; cursor: pointer; font-size: 16px; width: 100%; transition: background 0.3s ease; } .ask-btn:hover { background: #27ae60; } .ask-btn:disabled { background: #95a5a6; cursor: not-allowed; } .answer-container { margin-top: 20px; padding: 20px; background: white; border-radius: 6px; border-left: 4px solid #3498db; display: none; } .loading { display: none; text-align: center; margin: 20px 0; } .spinner { border: 3px solid #f3f3f3; border-top: 3px solid #3498db; border-radius: 50%; width: 30px; height: 30px; animation: spin 1s linear infinite; margin: 0 auto 10px; } keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .error { color: #e74c3c; margin-top: 10px; padding: 10px; background: #fdf2f2; border-radius: 4px; display: none; } footer { text-align: center; margin-top: 40px; padding-top: 20px; border-top: 1px solid #eaeaea; color: #7f8c8d; font-size: 0.9em; } /style /head body div classcontainer header h1Ostrakon-VL-8B 视觉语言模型演示/h1 p classsubtitle上传图片提出问题体验多模态AI的对话能力/p /header div classmain-content div classupload-section h21. 上传图片/h2 div classupload-area iduploadArea p点击选择或拖拽图片到此处/p p stylecolor: #7f8c8d; margin-top: 10px; font-size: 0.9em; 支持 PNG、JPG、JPEG、GIF 格式最大 5MB /p button classupload-btn onclickdocument.getElementById(fileInput).click() 选择图片 /button /div input typefile idfileInput acceptimage/* div classpreview-container img idimagePreview alt图片预览 /div div classerror iduploadError/div /div div classqa-section h22. 与图片对话/h2 textarea classquestion-input idquestionInput placeholder输入关于图片的问题例如图片里有什么描述一下这个场景。图中的人物在做什么... /textarea button classask-btn idaskBtn disabled 向模型提问 /button div classloading idloading div classspinner/div p模型正在思考中.../p /div div classanswer-container idanswerContainer h3模型回答/h3 p idanswerText/p /div div classerror idquestionError/div /div /div footer p本演示界面使用 Python Flask 构建 | Ostrakon-VL-8B 视觉语言模型/p p stylemargin-top: 5px; font-size: 0.8em; 提示这是一个快速原型演示实际使用时需要连接真实的模型API /p /footer /div script // 获取页面元素 const fileInput document.getElementById(fileInput); const uploadArea document.getElementById(uploadArea); const imagePreview document.getElementById(imagePreview); const uploadError document.getElementById(uploadError); const questionInput document.getElementById(questionInput); const askBtn document.getElementById(askBtn); const loading document.getElementById(loading); const answerContainer document.getElementById(answerContainer); const answerText document.getElementById(answerText); const questionError document.getElementById(questionError); let currentImagePath ; // 拖拽上传功能 uploadArea.addEventListener(dragover, (e) { e.preventDefault(); uploadArea.classList.add(dragover); }); uploadArea.addEventListener(dragleave, () { uploadArea.classList.remove(dragover); }); uploadArea.addEventListener(drop, (e) { e.preventDefault(); uploadArea.classList.remove(dragover); if (e.dataTransfer.files.length) { fileInput.files e.dataTransfer.files; handleFileSelect(); } }); // 文件选择处理 fileInput.addEventListener(change, handleFileSelect); function handleFileSelect() { const file fileInput.files[0]; if (!file) return; // 验证文件类型 const validTypes [image/png, image/jpeg, image/jpg, image/gif]; if (!validTypes.includes(file.type)) { showUploadError(请选择有效的图片文件PNG、JPG、JPEG、GIF); return; } // 验证文件大小5MB if (file.size 5 * 1024 * 1024) { showUploadError(文件大小不能超过5MB); return; } // 显示预览 const reader new FileReader(); reader.onload function(e) { imagePreview.src e.target.result; imagePreview.style.display block; uploadError.style.display none; // 上传到服务器 uploadFile(file); }; reader.readAsDataURL(file); } function uploadFile(file) { const formData new FormData(); formData.append(file, file); fetch(/upload, { method: POST, body: formData }) .then(response response.json()) .then(data { if (data.success) { currentImagePath data.filepath; askBtn.disabled false; askBtn.textContent 向模型提问; } else { showUploadError(data.error || 上传失败); } }) .catch(error { showUploadError(上传过程中出现错误 error.message); }); } function showUploadError(message) { uploadError.textContent message; uploadError.style.display block; askBtn.disabled true; } // 提问功能 askBtn.addEventListener(click, askQuestion); function askQuestion() { const question questionInput.value.trim(); if (!question) { showQuestionError(请输入问题); return; } if (!currentImagePath) { showQuestionError(请先上传图片); return; } // 清空之前的错误和回答 questionError.style.display none; answerContainer.style.display none; // 显示加载中 loading.style.display block; askBtn.disabled true; askBtn.textContent 思考中...; // 发送请求到服务器 fetch(/ask, { method: POST, headers: { Content-Type: application/json, }, body: JSON.stringify({ image_path: currentImagePath, question: question }) }) .then(response response.json()) .then(data { loading.style.display none; askBtn.disabled false; askBtn.textContent 向模型提问; if (data.error) { showQuestionError(data.error); } else { // 显示回答 answerText.textContent data.answer; answerContainer.style.display block; } }) .catch(error { loading.style.display none; askBtn.disabled false; askBtn.textContent 向模型提问; showQuestionError(请求失败 error.message); }); } function showQuestionError(message) { questionError.textContent message; questionError.style.display block; } // 按Enter键提问CtrlEnter换行 questionInput.addEventListener(keydown, (e) { if (e.key Enter !e.ctrlKey !e.shiftKey) { e.preventDefault(); if (!askBtn.disabled) { askQuestion(); } } }); /script /body /html这个HTML页面包含了一个完整的用户界面有图片上传区域、问题输入框、结果显示区域还有相应的JavaScript代码来处理用户交互。4. 运行和测试你的演示现在所有文件都准备好了让我们来运行这个应用。回到命令行确保你在ostrakon_demo文件夹里然后运行python app.py你会看到类似这样的输出* Serving Flask app app * Debug mode: on * Running on http://127.0.0.1:5000打开你的浏览器访问http://127.0.0.1:5000就能看到我们刚刚创建的演示界面了。试试看点击选择图片按钮上传一张图片在右侧的文本框中输入一个问题比如图片里有什么点击向模型提问按钮现在你会看到一个模拟的回答。这是因为我们在app.py里用的是模拟数据。接下来就是最关键的一步——连接真实的模型。5. 连接真实的Ostrakon-VL-8B模型要让这个演示真正工作起来你需要根据你的模型部署方式来修改app.py中的ask_question函数。这里我给你几个常见的连接方式5.1 如果模型部署为HTTP API假设你的Ostrakon-VL-8B模型已经通过类似FastAPI、Gradio或自定义服务部署成了一个HTTP API你可以这样修改app.route(/ask, methods[POST]) def ask_question(): 处理用户提问调用模型API data request.json image_path data.get(image_path, ) question data.get(question, ) if not image_path or not question: return jsonify({error: 请提供图片路径和问题}), 400 try: # 读取图片文件 full_image_path os.path.join(static, image_path.lstrip(/static/)) with open(full_image_path, rb) as f: image_data f.read() # 调用模型API # 注意这里的URL需要替换成你实际的模型API地址 api_url http://localhost:8000/predict # 修改为你的API地址 # 根据你的API要求调整请求格式 response requests.post(api_url, files{ image: image_data }, data{ question: question }, timeout30) if response.status_code 200: result response.json() return jsonify({answer: result.get(answer, 模型未返回有效答案)}) else: return jsonify({error: f模型API错误: {response.status_code}}), 500 except Exception as e: return jsonify({error: f调用模型时出错: {str(e)}}), 5005.2 如果模型在本地直接调用如果你直接在Python环境中加载了模型可以这样修改from transformers import AutoProcessor, AutoModelForVision2Seq from PIL import Image import torch # 在文件开头加载模型注意这会在启动时加载可能需要一些时间 processor None model None def load_model(): 加载模型只需要运行一次 global processor, model if processor is None or model is None: print(正在加载Ostrakon-VL-8B模型...) processor AutoProcessor.from_pretrained(your-model-path/ostrakon-vl-8b) model AutoModelForVision2Seq.from_pretrained(your-model-path/ostrakon-vl-8b) print(模型加载完成) return processor, model app.route(/ask, methods[POST]) def ask_question(): 处理用户提问直接调用本地模型 data request.json image_path data.get(image_path, ) question data.get(question, ) if not image_path or not question: return jsonify({error: 请提供图片路径和问题}), 400 try: # 加载模型第一次调用时会加载 processor, model load_model() # 读取图片 full_image_path os.path.join(static, image_path.lstrip(/static/)) image Image.open(full_image_path).convert(RGB) # 准备输入 # 注意根据Ostrakon-VL-8B的实际输入格式调整 inputs processor(imagesimage, textquestion, return_tensorspt) # 生成回答 with torch.no_grad(): generated_ids model.generate(**inputs, max_length100) answer processor.batch_decode(generated_ids, skip_special_tokensTrue)[0] return jsonify({answer: answer}) except Exception as e: return jsonify({error: f模型推理时出错: {str(e)}}), 5005.3 使用Gradio或Streamlit的API如果你的模型已经用Gradio或Streamlit部署了它们通常提供API接口app.route(/ask, methods[POST]) def ask_question(): 通过Gradio API调用模型 data request.json image_path data.get(image_path, ) question data.get(question, ) if not image_path or not question: return jsonify({error: 请提供图片路径和问题}), 400 try: # Gradio API调用示例 full_image_path os.path.join(static, image_path.lstrip(/static/)) # 假设Gradio服务运行在7860端口 gradio_url http://localhost:7860/api/predict response requests.post(gradio_url, json{ data: [ full_image_path, # 图片路径 question # 问题 ] }, timeout30) if response.status_code 200: result response.json() # Gradio返回的数据结构通常是 {data: [answer]} answer result.get(data, [])[0] return jsonify({answer: answer}) else: return jsonify({error: Gradio API调用失败}), 500 except Exception as e: return jsonify({error: f调用Gradio API时出错: {str(e)}}), 5006. 部署到服务器可选如果你想让别人也能访问这个演示可以把它部署到服务器上。这里有几个简单的选择6.1 使用云服务器如果你有云服务器比如阿里云、腾讯云、AWS等可以这样部署把整个ostrakon_demo文件夹上传到服务器在服务器上安装Python和依赖pip install flask pillow requests使用生产模式的WSGI服务器比如Gunicornpip install gunicorn gunicorn -w 4 -b 0.0.0.0:5000 app:app6.2 使用PythonAnywhere免费选项对于小型演示PythonAnywhere是个不错的选择注册PythonAnywhere免费账户上传你的代码文件创建一个新的Web应用选择Flask修改WSGI文件指向你的app.py点击重新加载你的应用就上线了6.3 使用Docker容器化如果你熟悉Docker可以创建一个DockerfileFROM python:3.9-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . CMD [gunicorn, --bind, 0.0.0.0:5000, app:app]然后构建和运行docker build -t ostrakon-demo . docker run -p 5000:5000 ostrakon-demo7. 一些实用的小技巧在实际使用中你可能会遇到一些问题这里分享几个小技巧图片太大上传慢怎么办可以在前端JavaScript里添加图片压缩功能或者在后端处理时调整图片尺寸from PIL import Image import io def compress_image(image_data, max_size(1024, 1024)): 压缩图片到指定尺寸 img Image.open(io.BytesIO(image_data)) img.thumbnail(max_size, Image.Resampling.LANCZOS) output io.BytesIO() img.save(output, formatJPEG, quality85) return output.getvalue()想支持更多文件格式修改app.py中的ALLOWED_EXTENSIONS配置app.config[ALLOWED_EXTENSIONS] {png, jpg, jpeg, gif, webp, bmp}需要用户认证可以添加简单的API密钥验证from functools import wraps def require_api_key(f): wraps(f) def decorated_function(*args, **kwargs): api_key request.headers.get(X-API-Key) if api_key ! your-secret-key-here: return jsonify({error: 无效的API密钥}), 401 return f(*args, **kwargs) return decorated_function app.route(/ask, methods[POST]) require_api_key # 添加这个装饰器 def ask_question(): # ... 原有代码想记录用户的使用情况可以添加简单的日志import logging from datetime import datetime logging.basicConfig(filenameapp.log, levellogging.INFO) app.route(/ask, methods[POST]) def ask_question(): # ... 原有代码 logging.info(f{datetime.now()} - 用户提问: {question}, 图片: {image_path}) # ... 原有代码8. 总结整个搭建过程其实比想象中简单主要就是三个文件一个Python后端、一个HTML前端页面再加上一点JavaScript处理交互。我用这种方式做过好几次技术演示效果都不错既展示了模型能力又显得很专业。Flask的好处是足够轻量改起来也方便。如果你想让界面更好看可以加一些CSS框架比如Bootstrap如果想加更多功能比如历史对话、多图上传也只需要在现有基础上扩展就行。实际用的时候最关键的是根据你的模型部署方式来调整ask_question函数。不同的部署方式本地直接调用、HTTP API、Gradio接口等需要不同的连接代码。如果遇到问题可以看看浏览器的开发者工具按F12里的网络请求通常能帮你找到问题所在。这个演示界面虽然简单但已经包含了核心功能上传图片、输入问题、显示回答。对于技术分享、项目演示或者内部测试来说完全够用了。而且因为代码都在你自己手里想怎么改就怎么改非常灵活。获取更多AI镜像想探索更多AI镜像和应用场景访问 CSDN星图镜像广场提供丰富的预置镜像覆盖大模型推理、图像生成、视频生成、模型微调等多个领域支持一键部署。

相关新闻