From a47b84e0091d5478da8233a592e2268cba8ea47a Mon Sep 17 00:00:00 2001 From: 24024 <240241002@qq.com> Date: Thu, 15 Jan 2026 21:42:03 +0800 Subject: [PATCH] =?UTF-8?q?feat(api):=20=E5=AE=9E=E7=8E=B0=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E7=94=9F=E6=88=90=E5=BC=82=E6=AD=A5=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E4=B8=8E=E4=BB=BB=E5=8A=A1=E7=8A=B6=E6=80=81=E6=9F=A5=E8=AF=A2?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增异步图片生成处理函数,支持后台任务执行及积分退还机制 - 实现任务状态查询接口,支持前端实时获取生成进度和结果 - 优化生成逻辑:根据模型类型分流,聊天模型同步调用,图片模型异步执行 - 调整积分预扣除和退还逻辑,保障用户积分安全 - 后台线程同步图片至私有存储,提升响应性能和用户体验 - 新增 /visualizer 路由对应前端控制器页面,辅助3D构图和拍摄角度设置 - 优化前端上传逻辑,新增设置器模式时单图上传限制 - 移除项目中未使用的前端脚本与配置文件,简化代码库维护 --- app.py | 4 + blueprints/__pycache__/api.cpython-312.pyc | Bin 21220 -> 20201 bytes blueprints/api.py | 255 +++++------- config.js | 26 -- logs/system.log | 32 ++ main.js | 415 ------------------- prompts.js | 103 ----- static/js/main.js | 195 +++++++-- templates/index.html | 37 +- templates/kongzhiqi.html | 460 +++++++++++++++++++++ 10 files changed, 798 insertions(+), 729 deletions(-) delete mode 100644 config.js delete mode 100644 main.js delete mode 100644 prompts.js create mode 100644 templates/kongzhiqi.html diff --git a/app.py b/app.py index a0809ba..eb9b284 100644 --- a/app.py +++ b/app.py @@ -33,6 +33,10 @@ def create_app(): def ocr(): return render_template('ocr.html') + @app.route('/visualizer') + def visualizer(): + return render_template('kongzhiqi.html') + # 自动创建数据库表 with app.app_context(): print("🔧 正在检查并创建数据库表...") diff --git a/blueprints/__pycache__/api.cpython-312.pyc b/blueprints/__pycache__/api.cpython-312.pyc index 976e61f4df83f5834c740f429ae72590b3aacfbb..d499527d56d5efa936dcb6178b6ea8eb645be1ba 100644 GIT binary patch delta 6199 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 ftz(Y`Jbr<|#j?jGGdefAu^q2N2@2@fF8nP;!3 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 B`-cDk diff --git a/blueprints/api.py b/blueprints/api.py index 67b70b1..d5d5c71 100644 --- a/blueprints/api.py +++ b/blueprints/api.py @@ -93,6 +93,63 @@ def sync_images_background(app, record_id, raw_urls): except Exception as e: print(f"❌ 更新记录失败: {e}") +def process_image_generation(app, user_id, task_id, payload, api_key, target_api, cost): + """异步执行图片生成并存入 Redis""" + with app.app_context(): + try: + headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} + # 使用较长的超时时间 (10分钟),确保长耗时任务不被中断 + resp = requests.post(target_api, json=payload, headers=headers, timeout=1000) + + if resp.status_code != 200: + # 错误处理:退还积分 + user = User.query.get(user_id) + if user and "sk-" in api_key: + user.points += cost + db.session.commit() + redis_client.setex(f"task:{task_id}", 3600, json.dumps({"status": "error", "message": resp.text})) + return + + api_result = resp.json() + raw_urls = [item['url'] for item in api_result.get('data', [])] + + # 持久化记录 + new_record = GenerationRecord( + user_id=user_id, + prompt=payload.get('prompt'), + model=payload.get('model'), + image_urls=json.dumps(raw_urls) + ) + db.session.add(new_record) + db.session.commit() + + # 后台线程处理:下载 AI 原始图片并同步到私有 MinIO + threading.Thread( + target=sync_images_background, + args=(app, new_record.id, raw_urls) + ).start() + + # 存入 Redis 标记完成 + redis_client.setex(f"task:{task_id}", 3600, json.dumps({"status": "complete", "urls": raw_urls})) + + except Exception as e: + # 异常处理:退还积分 + user = User.query.get(user_id) + if user and "sk-" in api_key: + user.points += cost + db.session.commit() + redis_client.setex(f"task:{task_id}", 3600, json.dumps({"status": "error", "message": str(e)})) + +@api_bp.route('/api/task_status/') +@login_required +def get_task_status(task_id): + """查询异步任务状态""" + data = redis_client.get(f"task:{task_id}") + if not data: + # 如果 Redis 里没有,可能是刚提交,也可能是过期了 + return jsonify({"status": "pending"}) + return jsonify(json.loads(data)) + @api_bp.route('/api/config') def get_config(): """从本地数据库字典获取配置""" @@ -157,197 +214,99 @@ def generate(): use_trial = False if mode == 'key': - # 自定义 Key 模式:优先使用本次输入的,否则使用数据库存的 api_key = input_key or user.api_key if not api_key: return jsonify({"error": "请先输入您的 API 密钥"}), 400 else: - # 积分/试用模式 if user.points > 0: - # 核心修复:优质模式使用专属 Key api_key = Config.PREMIUM_KEY if is_premium else Config.TRIAL_KEY target_api = Config.TRIAL_API use_trial = True else: return jsonify({"error": "可用积分已耗尽,请充值或切换至自定义 Key 模式"}), 400 - # 如果是 Key 模式且输入了新 Key,则自动更新到数据库保存 if mode == 'key' and input_key and input_key != user.api_key: user.api_key = input_key db.session.commit() - # 获取模型及对应的消耗积分 model_value = data.get('model') is_chat_model = "gemini" in model_value.lower() or "gpt" in model_value.lower() model_dict = SystemDict.query.filter_by(dict_type='ai_model', value=model_value).first() cost = model_dict.cost if model_dict else 1 - # 核心修复:优质模式积分消耗 X2 if use_trial and is_premium: cost *= 2 - # --- 积分预扣除逻辑 (点击即扣) --- if use_trial: if user.points < cost: - return jsonify({"error": f"可用积分不足,优质模式需要 {cost} 积分,您当前剩余 {user.points} 积分"}), 400 - + return jsonify({"error": f"可用积分不足"}), 400 user.points -= cost db.session.commit() - system_logger.info(f"积分预扣除 ({'优质' if is_premium else '普通'}试用)", phone=user.phone, cost=cost, remaining_points=user.points) - try: - prompt = data.get('prompt') - model = model_value - ratio = data.get('ratio') - size = data.get('size') - input_img_urls = data.get('image_urls', []) + prompt = data.get('prompt') + ratio = data.get('ratio') + size = data.get('size') + image_data = data.get('image_data', []) + + payload = { + "prompt": prompt, + "model": model_value, + "response_format": "url", + "aspect_ratio": ratio + } + if image_data: + payload["image"] = [img.split(',', 1)[1] if ',' in img else img for img in image_data] - payload = { - "prompt": prompt, - "model": model, - "response_format": "url", - "aspect_ratio": ratio - } - if input_img_urls: - payload["image"] = input_img_urls - - if model == "nano-banana-2" and size: - payload["image_size"] = size - - # 如果是聊天模型,重新构建 payload - if is_chat_model: - messages = data.get('messages') - - # 核心修复:将图片 URL 转换为 Base64,解决第三方接口禁止 9000 端口访问的问题 - def url_to_base64(url): - if not url or not url.startswith('http'): - return url - if ':9000' not in url: - return url - try: - # 尝试通过 S3 客户端直接读取(更安全,避开网络回环问题) - filename = url.split('/')[-1] - from urllib.parse import unquote - filename = unquote(filename) - - resp = s3_client.get_object(Bucket=Config.MINIO["bucket"], Key=filename) - content = resp['Body'].read() - mime_type = resp.get('ContentType', 'image/jpeg') - - encoded_string = base64.b64encode(content).decode('utf-8') - return f"data:{mime_type};base64,{encoded_string}" - except Exception as e: - print(f"⚠️ Base64 转换失败: {e}") - return url - - if not messages: - # 兼容性处理:如果没有 messages,构建一个简单的 - messages = [ - {"role": "system", "content": data.get('system_prompt', "You are a helpful assistant.")}, - {"role": "user", "content": [ - {"type": "text", "text": prompt} - ]} - ] - for img_url in input_img_urls: - messages[1]["content"].append({"type": "image_url", "image_url": {"url": url_to_base64(img_url)}}) - else: - # 递归处理传入的 messages - for msg in messages: - content = msg.get('content') - if isinstance(content, list): - for item in content: - if isinstance(item, dict) and item.get('type') == 'image_url': - img_info = item.get('image_url') - if img_info and 'url' in img_info: - img_info['url'] = url_to_base64(img_info['url']) - - payload = { - "model": model, - "messages": messages, - "stream": False - } - target_api = Config.CHAT_API - if not mode == 'key': - # 使用生图默认的试用/优质 Key - api_key = Config.PREMIUM_KEY if is_premium else Config.TRIAL_KEY + if model_value == "nano-banana-2" and size: + payload["image_size"] = size + # 如果是聊天模型,直接同步处理 + if is_chat_model: headers = {"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"} - resp = requests.post(target_api, json=payload, headers=headers, timeout=300) - + chat_payload = { + "model": model_value, + "messages": [{"role": "user", "content": prompt}] + } + resp = requests.post(Config.CHAT_API, json=chat_payload, headers=headers, timeout=120) if resp.status_code != 200: - # API 报错,退还积分 if use_trial: user.points += cost db.session.commit() - system_logger.warning(f"API 报错,积分已退还", phone=user.phone, status_code=resp.status_code) return jsonify({"error": resp.text}), resp.status_code - + api_result = resp.json() + content = api_result['choices'][0]['message']['content'] - if is_chat_model: - # 聊天模型返回的是文本 - content = api_result['choices'][0]['message']['content'] - - # 核心修复:如果是验光单解读(OCR),不存入生图历史记录,避免污染生图历史 - if prompt != "解读验光单": - new_record = GenerationRecord( - user_id=session.get('user_id'), - prompt=prompt, - model=model, - image_urls=json.dumps([{"type": "text", "content": content}]) - ) - db.session.add(new_record) - db.session.commit() - system_logger.info(f"用户生成文本成功", phone=user.phone, model=model, record_id=new_record.id) - else: - system_logger.info(f"用户解析验光单成功", phone=user.phone, model=model) - - return jsonify({ - "data": [{"content": content, "type": "text"}], - "message": "解析成功!" - }) - - raw_urls = [item['url'] for item in api_result.get('data', [])] + # 记录聊天历史 + if prompt != "解读验光单": + new_record = GenerationRecord( + user_id=user_id, + prompt=prompt, + model=model_value, + image_urls=json.dumps([{"type": "text", "content": content}]) + ) + db.session.add(new_record) + db.session.commit() - # 立即写入数据库(先存原始 URL) - new_record = GenerationRecord( - user_id=session.get('user_id'), - prompt=prompt, - model=model, - image_urls=json.dumps(raw_urls) - ) - db.session.add(new_record) - db.session.commit() - - # 写入系统日志 - system_logger.info( - f"用户生成图片成功", - phone=user.phone, - model=model, - record_id=new_record.id - ) - - # 启动后台线程同步图片,不阻塞前端返回 - app = current_app._get_current_object() - threading.Thread( - target=sync_images_background, - args=(app, new_record.id, raw_urls) - ).start() - - # 立即返回原始 URL 给前端展示 return jsonify({ - "data": [{"url": url} for url in raw_urls], - "message": "生成成功!作品正在后台同步至云存储。" + "data": [{"content": content, "type": "text"}], + "message": "生成成功!" }) - except Exception as e: - # 发生系统异常,退还积分 - if use_trial: - user.points += cost - db.session.commit() - system_logger.error(f"生成异常,积分已退还", phone=user.phone, error=str(e)) - return jsonify({"error": str(e)}), 500 + # --- 异步处理生图任务 --- + task_id = str(uuid.uuid4()) + app = current_app._get_current_object() + + threading.Thread( + target=process_image_generation, + args=(app, user_id, task_id, payload, api_key, target_api, cost) + ).start() + + return jsonify({ + "task_id": task_id, + "message": "已开启异步生成任务" + }) except Exception as e: return jsonify({"error": str(e)}), 500 diff --git a/config.js b/config.js deleted file mode 100644 index 22339d6..0000000 --- a/config.js +++ /dev/null @@ -1,26 +0,0 @@ -// config.js -const AppConfig = { - API_URL: 'https://ai.comfly.chat/v1/chat/completions', - //MODEL: 'gemini-3-pro-preview', // 你要求的模型 - //MODEL: 'gemini-2.5-pro', - MODEL: 'gemini-3-flash-preview', - // 这里填入第一步生成的“加密Key” - ENCRYPTED_KEY: "MARAXis7BiwzBDwiLQIjLHNRWwwbWQAKDSFyAiQEYx8iIy8QUHNRAQkoJhMdCRRRWAwJ", - - // 解密函数(必须与加密算法对应) - getDecryptedKey: function() { - const salt = "ComflyChatSecret2025"; // 必须与加密时的 Salt 一致 - try { - const raw = atob(this.ENCRYPTED_KEY); - let result = ""; - for (let i = 0; i < raw.length; i++) { - const charCode = raw.charCodeAt(i) ^ salt.charCodeAt(i % salt.length); - result += String.fromCharCode(charCode); - } - return result; - } catch (e) { - console.error("Key 解密失败"); - return ""; - } - } -}; \ No newline at end of file diff --git a/logs/system.log b/logs/system.log index 51055a5..8de7ffb 100644 --- a/logs/system.log +++ b/logs/system.log @@ -134,3 +134,35 @@ [2026-01-14 20:12:31] INFO - 用户登录成功 [2026-01-14 20:17:10] INFO - 用户登录尝试 [2026-01-14 20:17:10] INFO - 用户登录成功 +[2026-01-15 20:17:15] INFO - 用户登录尝试 +[2026-01-15 20:17:15] WARNING - 登录失败: 手机号或密码错误 +[2026-01-15 20:17:17] INFO - 用户登录尝试 +[2026-01-15 20:17:17] WARNING - 登录失败: 手机号或密码错误 +[2026-01-15 20:17:21] INFO - 用户登录尝试 +[2026-01-15 20:17:22] WARNING - 登录失败: 手机号或密码错误 +[2026-01-15 20:17:32] INFO - 用户登录尝试 +[2026-01-15 20:17:32] INFO - 用户登录成功 +[2026-01-15 20:22:42] INFO - 用户生成图片成功 +[2026-01-15 20:31:31] INFO - 用户生成图片成功 +[2026-01-15 20:45:22] INFO - 用户生成图片成功 +[2026-01-15 20:49:12] INFO - 用户生成图片成功 +[2026-01-15 20:50:53] INFO - 用户生成图片成功 +[2026-01-15 20:52:35] INFO - 用户生成图片成功 +[2026-01-15 20:52:35] INFO - 用户生成图片成功 +[2026-01-15 20:52:37] INFO - 用户生成图片成功 +[2026-01-15 20:52:37] INFO - 用户生成图片成功 +[2026-01-15 20:56:42] INFO - 用户生成图片成功 +[2026-01-15 20:57:31] INFO - 用户生成图片成功 +[2026-01-15 20:59:14] INFO - 用户生成图片成功 +[2026-01-15 21:00:00] INFO - 用户生成图片成功 +[2026-01-15 21:01:05] INFO - 用户生成图片成功 +[2026-01-15 21:01:05] INFO - 用户生成图片成功 +[2026-01-15 21:01:05] INFO - 用户生成图片成功 +[2026-01-15 21:05:31] INFO - 用户生成图片成功 +[2026-01-15 21:05:31] INFO - 用户生成图片成功 +[2026-01-15 21:05:31] INFO - 用户生成图片成功 +[2026-01-15 21:05:31] INFO - 用户生成图片成功 +[2026-01-15 21:08:09] INFO - 积分预扣除 (普通试用) +[2026-01-15 21:09:55] INFO - 用户生成图片成功 (Base64模式) +[2026-01-15 21:13:38] INFO - 积分预扣除 (普通试用) +[2026-01-15 21:13:59] INFO - 用户生成图片成功 (上传:Base64 / 接收:URL) diff --git a/main.js b/main.js deleted file mode 100644 index 236fa59..0000000 --- a/main.js +++ /dev/null @@ -1,415 +0,0 @@ -// main.js - -// 状态管理 -let imageList = []; -let finalMarkdown = ""; -let extractedData = null; -let statusInterval = null; // 用于动态文字切换 - -// DOM 元素引用 -const els = { - dropArea: document.getElementById('drop-area'), - fileInput: document.getElementById('file-input'), - placeholder: document.getElementById('upload-placeholder'), - previewGrid: document.getElementById('preview-grid'), - controlBar: document.getElementById('control-bar'), - imgCount: document.getElementById('img-count'), - clearBtn: document.getElementById('clear-btn'), - analyzeBtn: document.getElementById('analyze-btn'), - btnText: document.getElementById('btn-text'), - btnSpinner: document.getElementById('btn-spinner'), - - // 结果区域视图 - placeholderView: document.getElementById('placeholder-view'), // 初始欢迎页 - loadingView: document.getElementById('loading-view'), // 加载动画页 - loadingStatus: document.getElementById('loading-status-text'), // 动态提示文字 - resultContent: document.getElementById('result-content'), // 结果显示页 - resultScroll: document.getElementById('result-scroll'), // 滚动容器 - copyBtn: document.getElementById('copy-btn') -}; - -// === 事件监听 === - -els.dropArea.addEventListener('click', (e) => { - if (e.target !== els.clearBtn && imageList.length < 2) els.fileInput.click(); -}); - -els.fileInput.addEventListener('change', e => handleFiles(e.target.files)); - -document.addEventListener('paste', e => { - const items = e.clipboardData.items; - const files = []; - for (let item of items) { - if (item.type.indexOf('image') !== -1) files.push(item.getAsFile()); - } - if (files.length > 0) handleFiles(files); -}); - -['dragenter', 'dragover', 'dragleave', 'drop'].forEach(name => { - els.dropArea.addEventListener(name, e => { e.preventDefault(); e.stopPropagation(); }); -}); -els.dropArea.addEventListener('drop', e => handleFiles(e.dataTransfer.files)); - -els.clearBtn.addEventListener('click', (e) => { - e.stopPropagation(); - resetImages(); -}); - -// === 复制按钮逻辑 === -els.copyBtn.addEventListener('click', async () => { - let textToCopy = ""; - - if (!extractedData) { - textToCopy = els.resultContent.innerText; - showCopyFeedback("⚠️ 未识别到数据,已复制全文"); - } else { - textToCopy = generateCopyString(extractedData); - } - - const success = await copyToClipboard(textToCopy); - - if (success) { - showCopyFeedback(extractedData ? "✅ 格式化数据已复制" : "✅ 已复制全文"); - } else { - prompt("浏览器禁止自动复制,请手动复制以下内容:", textToCopy); - } -}); - -// === 兼容性极强的复制函数 === -async function copyToClipboard(text) { - try { - await navigator.clipboard.writeText(text); - return true; - } catch (err) { - console.warn("现代复制API失败,尝试传统方法:", err); - try { - const textarea = document.createElement('textarea'); - textarea.value = text; - textarea.style.position = 'fixed'; - textarea.style.left = '-9999px'; - textarea.style.top = '0'; - textarea.setAttribute('readonly', ''); - document.body.appendChild(textarea); - textarea.focus(); - textarea.select(); - const successful = document.execCommand('copy'); - document.body.removeChild(textarea); - return successful; - } catch (e) { - return false; - } - } -} - -function showCopyFeedback(msg) { - const originalText = "📄 复制结果"; - els.copyBtn.innerText = msg; - setTimeout(() => { - els.copyBtn.innerText = originalText; - }, 2000); -} - -// === 高度定制的复制逻辑 === -function generateCopyString(data) { - const r = data.right; - const l = data.left; - const tPd = data.total_pd; - - const blocks = []; - - const fmtDeg = (val) => { - if (!val || val === "未标明" || val.trim() === "") return null; - const num = parseFloat(val); - if (isNaN(num)) return null; - const deg = Math.round(num * 100); - if (deg === 0) return null; - if (deg < 0) return Math.abs(deg).toString(); - if (deg > 0) return "+" + deg.toString(); - return null; - }; - - const fmtCyl = (val) => fmtDeg(val); - const fmtRaw = (val) => (!val || val === "未标明" || val.trim() === "") ? null : val.trim(); - const fmtPD = (val) => { - if (!val || val === "未标明" || val.trim() === "") return null; - const num = parseFloat(val); - if (isNaN(num)) return val.trim(); - return num.toString(); - }; - - const rS = fmtDeg(r.s), lS = fmtDeg(l.s); - const rC = fmtCyl(r.c), lC = fmtCyl(l.c); - const rA = rC ? fmtRaw(r.a) : null, lA = lC ? fmtRaw(l.a) : null; - const rAdd = fmtDeg(r.add), lAdd = fmtDeg(l.add); - const rPH = fmtRaw(r.ph), lPH = fmtRaw(l.ph); - const rPd = fmtPD(r.pd), lPd = fmtPD(l.pd), totalPd = fmtPD(tPd); - - if (!rS && !rC && !lS && !lC) return "平光"; - - const isDoubleSame = (rS === lS) && (rC === lC) && (rA === lA); - - if (isDoubleSame) { - let block = `双眼${rS || ""}`; - if (rC) block += `散光${rC}`; - if (rA) block += `轴位${rA}`; - blocks.push(block); - } else { - if (rS || rC) { - let block = `右眼${rS || ""}`; - if (rC) block += `散光${rC}`; - if (rA) block += `轴位${rA}`; - blocks.push(block); - } - if (lS || lC) { - let block = `左眼${lS || ""}`; - if (lC) block += `散光${lC}`; - if (lA) block += `轴位${lA}`; - blocks.push(block); - } - } - - if (rAdd && lAdd && rAdd === lAdd) blocks.push(`ADD${rAdd}`); - else { - if (rAdd) blocks.push(`ADD${rAdd}`); - if (lAdd) blocks.push(`ADD${lAdd}`); - } - - if (rPH) blocks.push(`瞳高右眼${rPH}`); - if (lPH) blocks.push(`瞳高左眼${lPH}`); - - if (totalPd) blocks.push(`瞳距${totalPd}`); - else { - if (rPd) blocks.push(`右眼瞳距${rPd}`); - if (lPd) blocks.push(`左眼瞳距${lPd}`); - } - - return blocks.join(' '); -} - -// === 核心逻辑函数 === -async function handleFiles(files) { - if (imageList.length >= 2) { - alert("最多只能上传 2 张图片,请先清空后再试。"); - return; - } - const remainingSlots = 2 - imageList.length; - const filesProcess = Array.from(files).slice(0, remainingSlots); - - for (let file of filesProcess) { - if (!file.type.startsWith('image/')) continue; - try { - const base64 = await readFileAsBase64(file); - const compressed = await compressImage(base64, 1024, 0.8); - imageList.push(compressed); - } catch (err) { - console.error("图片处理失败", err); - } - } - updatePreviewUI(); -} - -function resetImages() { - imageList = []; - updatePreviewUI(); - els.fileInput.value = ''; -} - -function updatePreviewUI() { - const count = imageList.length; - if (count === 0) { - els.placeholder.classList.remove('hidden'); - els.previewGrid.classList.add('hidden'); - els.controlBar.classList.add('hidden'); - } else { - els.placeholder.classList.add('hidden'); - els.previewGrid.classList.remove('hidden'); - els.controlBar.classList.remove('hidden'); - } - els.previewGrid.innerHTML = ''; - els.previewGrid.className = `w-full h-full grid gap-2 ${count > 1 ? 'grid-cols-2' : 'grid-cols-1'}`; - imageList.forEach((imgSrc) => { - const div = document.createElement('div'); - div.className = "relative rounded-lg overflow-hidden border border-slate-200 bg-slate-100 flex items-center justify-center h-full max-h-[400px]"; - div.innerHTML = ``; - els.previewGrid.appendChild(div); - }); - els.imgCount.innerText = `${count}/2`; -} - -function readFileAsBase64(file) { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = (e) => resolve(e.target.result); - reader.onerror = reject; - reader.readAsDataURL(file); - }); -} - -function compressImage(base64Str, maxDim, quality) { - return new Promise((resolve) => { - const img = new Image(); - img.src = base64Str; - img.onload = () => { - let w = img.width, h = img.height; - if (w > maxDim || h > maxDim) { - if (w > h) { h = Math.round(h * maxDim / w); w = maxDim; } - else { w = Math.round(w * maxDim / h); h = maxDim; } - } - const canvas = document.createElement('canvas'); - canvas.width = w; canvas.height = h; - const ctx = canvas.getContext('2d'); - ctx.drawImage(img, 0, 0, w, h); - resolve(canvas.toDataURL('image/jpeg', quality)); - }; - }); -} - -// === 核心:设置加载状态 UI === -function setLoading(isLoading) { - els.analyzeBtn.disabled = isLoading; - - // 清除之前的定时器 - if (statusInterval) { - clearInterval(statusInterval); - statusInterval = null; - } - - if (isLoading) { - els.btnText.innerText = "正在分析..."; - els.btnSpinner.classList.remove('hidden'); - - if (els.placeholderView) els.placeholderView.classList.add('hidden'); - if (els.resultContent) els.resultContent.classList.add('hidden'); - if (els.loadingView) els.loadingView.classList.remove('hidden'); - - const statuses = [ - "正在上传并扫描验光单...", - "AI 正在识别视觉参数...", - "正在校对球镜与柱镜数值...", - "正在进行深度光学分析...", - "正在生成专业解读报告...", - "正在整理结果排版..." - ]; - - let i = 0; - if (els.loadingStatus) { - els.loadingStatus.innerText = statuses[0]; - - statusInterval = setInterval(() => { - if (i < statuses.length - 1) { - i++; - els.loadingStatus.innerText = statuses[i]; - } else { - // 播完了还没返回?进入“深度思考”省略号动画模式 - clearInterval(statusInterval); - let dots = 0; - const baseText = statuses[statuses.length - 1]; - statusInterval = setInterval(() => { - dots = (dots + 1) % 4; - els.loadingStatus.innerText = baseText + ".".repeat(dots); - }, 500); - } - }, 3000); // 3秒切换一次,更真实 - } - } else { - els.btnText.innerText = "开始智能解读"; - els.btnSpinner.classList.add('hidden'); - - if (els.loadingView) els.loadingView.classList.add('hidden'); - if (els.resultContent) els.resultContent.classList.remove('hidden'); - } -} - -// === API 调用逻辑 === -els.analyzeBtn.addEventListener('click', async () => { - if (imageList.length === 0) return alert('请至少上传 1 张验光单'); - - const apiKey = AppConfig.getDecryptedKey(); - if (!apiKey) return alert("配置错误:无法读取 API Key"); - - setLoading(true); - finalMarkdown = ""; - extractedData = null; - els.resultContent.innerHTML = ""; - - try { - const userContent = AppPrompts.generateUserPayload(imageList); - - const response = await fetch(AppConfig.API_URL, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${apiKey}` - }, - body: JSON.stringify({ - model: AppConfig.MODEL, - stream: true, - messages: [ - { role: "system", content: AppPrompts.systemMessage }, - { role: "user", content: userContent } - ] - }) - }); - - if (!response.ok) throw new Error(`API 请求失败: ${response.status}`); - - const reader = response.body.getReader(); - const decoder = new TextDecoder(); - - let hasStartedStreaming = false; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - const chunk = decoder.decode(value, { stream: true }); - const lines = chunk.split('\n'); - - for (const line of lines) { - if (line.startsWith('data: ') && line !== 'data: [DONE]') { - try { - const json = JSON.parse(line.substring(6)); - const content = json.choices[0].delta?.content || ""; - - if (content && !hasStartedStreaming) { - hasStartedStreaming = true; - // 数据回来的一瞬间,立即停止加载动画和文字 - if (els.loadingView) els.loadingView.classList.add('hidden'); - els.resultContent.classList.remove('hidden'); - if (statusInterval) clearInterval(statusInterval); - } - - finalMarkdown += content; - - // 实时解析 Markdown (过滤 JSON 块) - const displayMarkdown = finalMarkdown.replace(/```json[\s\S]*```/, ''); - els.resultContent.innerHTML = marked.parse(displayMarkdown); - - // 自动滚动到底部 - if (els.resultScroll) { - els.resultScroll.scrollTop = els.resultScroll.scrollHeight; - } - } catch (e) {} - } - } - } - - // 结束后提取 JSON 用于复制功能 - const jsonMatch = finalMarkdown.match(/```json\n([\s\S]*?)\n```/); - if (jsonMatch && jsonMatch[1]) { - try { - extractedData = JSON.parse(jsonMatch[1]); - } catch (e) {} - } - - els.copyBtn.disabled = false; - els.copyBtn.classList.remove('opacity-50', 'cursor-not-allowed'); - - } catch (err) { - els.resultContent.innerHTML = `
出错了: ${err.message}
`; - if (els.loadingView) els.loadingView.classList.add('hidden'); - els.resultContent.classList.remove('hidden'); - } finally { - setLoading(false); - } -}); \ No newline at end of file diff --git a/prompts.js b/prompts.js deleted file mode 100644 index 1e0123a..0000000 --- a/prompts.js +++ /dev/null @@ -1,103 +0,0 @@ -// prompts.js - -const AppPrompts = { - systemMessage: `你是一个专业的眼科验光师助手,服务对象是眼镜店客服人员。 - -1. **基本原则**: - - 使用简体中文。 - - 核心任务:精准提取配镜数据(S, C, A, PD)。 - - **全能模式**:你需要同时具备处理“电脑验光单”、“手写处方”、“光学十字图”的能力。 - -2. **【⚠️ 核心逻辑一:视觉符号与笔迹语义】(完整保留,严禁简化)** - 请仔细观察图像中的手写标记,它们决定了数据的取舍: - * **标记 A:下划线/圈选 (Row Selection)** —— **针对电脑验光单** - * **现象**:在打印数据列表(非 AVE 行)的**某一行数字下方**画了横线,或圈出了某一行。 - * **含义**:验光师认为该行数据最准,**强制放弃**底部的 AVE(平均值)。 - * **操作**:直接提取**被标记行**的 CYL (柱镜) 和 AX (轴位)。即使 AVE 行有数据,也以被标记的行为准。 - * **标记 B:删除线 (Strikethrough/Cancellation)** - * **现象**:线条直接**贯穿/覆盖**了数字(常见于散光/轴位)。 - * **操作**:该项数据归零(如散光变 0.00)。 - * **标记 C:手写数值 (Handwritten Override)** - * **现象**:旁边写了新的数字(如 -0.75)。 - * **操作**:手写数值优先级最高,直接覆盖打印数值。 - -3. **【⚠️ 核心逻辑二:光学十字换算】(完整保留,严禁简化)** - 若图像中出现手画的“十字交叉图”,请严格执行: - * **定球镜 (S)**:取十字线上两个数值中,代数较大(更偏正/更不负)的数值作为 S。 - * **定轴位 (A)**: - - 找到标有角度数值(如 90°, 180°)的那根线。 - - 若 S 在该线上,A = 该角度。 - - 若 S 不在该线上,A = 该角度 ± 90°。 - * **定柱镜 (C)**:C = (十字线上另一个数值) - S。 - -4. **【数据提取优先级序列】** - 请按以下顺序确定最终数值: - 1. **手写修正的具体数值** (最高) - 2. **被下划线/圈选的特定打印行** (次高) - 3. **打印的 AVE (平均值)** (普通) - 4. **光学十字推算值** (仅在有十字图时生效) - -5. **【⚠️ 智能诊断与客服话术生成】(功能升级)** - 在精准提取数据后,请检查是否命中以下规则。 - **执行要求**:若命中,请在输出结果的第三部分,直接生成一段**客服可以直接复制发送给客户的回复话术**。 - *(若同时命中多条,请将话术自然融合,不要机械分点)* - - * **场景 A:非标准步长安抚(数值不能被 0.25 整除,如 -0.12, -0.37)** - * **触发条件**:提取的 S 或 C 结尾不是 .00, .25, .50, .75。 - * **话术策略**:安抚客户不用担心“奇怪的数字”,解释这是电脑原始数据,承诺会按标准(0.25档)调整。 - * **话术示例**(仅供参考): - "亲,单子上显示的 -0.37 是电脑验光的原始参考值哈。我们配镜时会按照国际标准度数(比如 0.25 或 0.50),您看您这边有没有验光师的手写单子呢,这样配出来的眼镜佩戴舒适哦~" - - * **场景 B:低度数/防蓝光推荐(双眼度数 < 0.25)** - * **触发条件**:双眼 S 和 C 绝对值均 < 0.25(含 0.00, 0.12, 0.25, 0.37)。 - * **话术策略**:恭喜客户视力底子好 -> 建议不配度数 -> 强烈推荐 0 度防蓝光(保护视力)。 - * **话术示例**(仅供参考): - "亲,您的视力底子非常好!数据看都只是极其微小的生理波动(不到 25 度),这通常是不需要配度数的。 - 如果您平时看手机电脑多,特别推荐您配一副【0度防蓝光眼镜】,既能阻隔辐射保护这么好的视力,平时戴着也很好看呢!" - - * **场景 C:PD (瞳距) 缺失** - * **触发条件**:手写区无 PD 且全图未扫描到 PD。 - * **话术策略**:温柔追问数据。 - * **话术示例**:"亲,单子上没看到瞳距(PD)数据哦,您之前有测过吗?或者手边有旧眼镜我们可以帮您测一下~" - -6. **【输出结构】** - 请严格按照以下顺序输出: - - **第一部分:分析摘要**。简述数据来源(如:“依据下划线提取了第3行数据...”)。 - - **第二部分:Markdown 表格**。包含:眼别, 球镜(S), 柱镜(C), 轴位(A), ADD, 瞳高(PH), 瞳距(PD)。 - - **第三部分:建议客服回复的话术**。 - * **关键**:请在此处输出 Point 5 中生成的**针对性话术**。 - * **格式**:不要写“规则A触发”,直接写:“建议您这样回复客户:xxxxxxxx”。 - * **融合要求**:如果同时有【非标准步长】和【低度数】,请生成一段包含这两点意思的完整话术。 - - **第四部分:JSON 数据**(必须在最后)。 - -7. **【JSON 输出格式】** -\`\`\`json -{ - "right": { "s": "", "c": "", "a": "", "add": "", "ph": "", "pd": "" }, - "left": { "s": "", "c": "", "a": "", "add": "", "ph": "", "pd": "" }, - "total_pd": "" -} -\`\`\` -`, - - generateUserPayload: function(imageArray) { - return [ - { - type: "text", - text: `请解读这张验光单。 -**执行步骤**: -1. **精准提取**:严格执行【核心逻辑一】(视觉符号)和【核心逻辑二】(光学十字),确保数据绝对准确。 -2. **话术生成(重要)**: - - 如果遇到 -0.12/-0.37 这种数,请生成一段话术,让客服告诉客户“这是电脑原始值,镜片的国际标准是0.25一档,可以询问客户有没有手写验光单”。 - - 如果遇到低度数(< 0.25),请生成一段话术,让客服推荐“0度防蓝光眼镜”。 - - **最终输出**:请在第三部分直接给出一代**客服可以复制发给客户的文字**,语气要亲切、自然。 -3. **PD 检查**:寻找打印的瞳距值。 -请输出最终结果。` - }, - ...imageArray.map(imgBase64 => ({ - type: "image_url", - image_url: { url: imgBase64, detail: "high" } - })) - ]; - } -}; \ No newline at end of file diff --git a/static/js/main.js b/static/js/main.js index 250dcd1..0a038fd 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -56,6 +56,7 @@ let isHistoryLoading = false; let currentGeneratedUrls = []; let currentMode = 'trial'; // 'trial' 或 'key' let uploadedFiles = []; // 存储当前待上传的参考图 +let isSetterActive = false; // 是否激活了拍摄角度设置器模式 function switchMode(mode) { currentMode = mode; @@ -180,6 +181,12 @@ async function init() { const closeHistoryBtn = document.getElementById('closeHistoryBtn'); const historyList = document.getElementById('historyList'); + // 3D 构图辅助控制 + const openVisualizerBtn = document.getElementById('openVisualizerBtn'); + const closeVisualizerBtn = document.getElementById('closeVisualizerBtn'); + if(openVisualizerBtn) openVisualizerBtn.onclick = openVisualizerModal; + if(closeVisualizerBtn) closeVisualizerBtn.onclick = closeVisualizerModal; + if(showHistoryBtn) { showHistoryBtn.onclick = () => { historyDrawer.classList.remove('translate-x-full'); @@ -339,14 +346,22 @@ function handleNewFiles(files) { const newFiles = Array.from(files).filter(f => f.type.startsWith('image/')); if (newFiles.length === 0) return; - if (uploadedFiles.length + newFiles.length > 3) { - showToast('最多只能上传 3 张参考图', 'warning'); - const remaining = 3 - uploadedFiles.length; - if (remaining > 0) { - uploadedFiles = uploadedFiles.concat(newFiles.slice(0, remaining)); + // 如果处于设置器模式,严格限制为 1 张 + if (isSetterActive) { + if (newFiles.length > 0) { + uploadedFiles = [newFiles[0]]; + showToast('设置器模式已开启,仅保留第一张参考图', 'info'); } } else { - uploadedFiles = uploadedFiles.concat(newFiles); + if (uploadedFiles.length + newFiles.length > 3) { + showToast('最多只能上传 3 张参考图', 'warning'); + const remaining = 3 - uploadedFiles.length; + if (remaining > 0) { + uploadedFiles = uploadedFiles.concat(newFiles.slice(0, remaining)); + } + } else { + uploadedFiles = uploadedFiles.concat(newFiles); + } } renderImagePreviews(); } @@ -448,16 +463,18 @@ document.getElementById('submitBtn').onclick = async () => { currentGeneratedUrls = []; // 重置当前生成列表 try { - let image_urls = []; + let image_data = []; - // 1. 如果有图则先上传 + // 1. 将图片转换为 Base64 if (uploadedFiles.length > 0) { - const uploadData = new FormData(); - for(let f of uploadedFiles) uploadData.append('images', f); - const upR = await fetch('/api/upload', { method: 'POST', body: uploadData }); - const upRes = await upR.json(); - if(upRes.error) throw new Error(upRes.error); - image_urls = upRes.urls; + btnText.innerText = "正在准备图片数据..."; + const readFileAsBase64 = (file) => new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => resolve(reader.result); + reader.onerror = reject; + reader.readAsDataURL(file); + }); + image_data = await Promise.all(uploadedFiles.map(f => readFileAsBase64(f))); } // 2. 并行启动多个生成任务 @@ -467,10 +484,11 @@ document.getElementById('submitBtn').onclick = async () => { const startTask = async (index) => { const slot = document.createElement('div'); slot.className = 'image-frame relative bg-white/50 animate-pulse min-h-[200px] flex items-center justify-center rounded-[2.5rem] border border-slate-100 shadow-sm'; - slot.innerHTML = `
AI 构思中...
`; + slot.innerHTML = `
正在排队中...
`; grid.appendChild(slot); try { + // 1. 发起生成请求,获取任务 ID const r = await fetch('/api/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -482,17 +500,64 @@ document.getElementById('submitBtn').onclick = async () => { model: document.getElementById('modelSelect').value, ratio: document.getElementById('ratioSelect').value, size: document.getElementById('sizeSelect').value, - image_urls + image_data // 发送 Base64 数组 }) }); const res = await r.json(); if(res.error) throw new Error(res.error); - if(res.message) showToast(res.message, 'success'); + // 如果直接返回了 data (比如聊天模型),直接显示 + if (res.data) { + displayResult(slot, res.data[0]); + return; + } - const imgUrl = res.data[0].url; - currentGeneratedUrls.push(imgUrl); - + // 2. 轮询任务状态 + const taskId = res.task_id; + let pollCount = 0; + const maxPolls = 500; // 最多轮询约 16 分钟 (2s * 500 = 1000s) + + while (pollCount < maxPolls) { + await new Promise(resolve => setTimeout(resolve, 2000)); + pollCount++; + + const statusR = await fetch(`/api/task_status/${taskId}`); + const statusRes = await statusR.json(); + + if (statusRes.status === 'complete') { + const imgUrl = statusRes.urls[0]; + currentGeneratedUrls.push(imgUrl); + displayResult(slot, { url: imgUrl }); + finishedCount++; + btnText.innerText = `AI 构思中 (${finishedCount}/${num})...`; + if(currentMode === 'trial') checkAuth(); + return; // 任务正常结束 + } else if (statusRes.status === 'error') { + throw new Error(statusRes.message || "生成失败"); + } else { + // 继续轮询状态显示 + slot.innerHTML = `
AI 正在努力创作中 (${pollCount * 2}s)...
`; + } + } + throw new Error("生成超时,请稍后在历史记录中查看"); + + } catch (e) { + slot.className = 'image-frame relative bg-red-50/50 flex items-center justify-center rounded-[2.5rem] border border-red-100 p-6'; + if(e.message.includes('401') || e.message.includes('请先登录')) { + slot.innerHTML = `
登录已过期,请重新登录
`; + } else { + slot.innerHTML = `
生成异常: ${e.message}
`; + } + } + }; + + // 提取结果展示逻辑 + const displayResult = (slot, data) => { + if (data.type === 'text') { + slot.className = 'image-frame relative bg-white border border-slate-100 p-8 rounded-[2.5rem] shadow-xl overflow-y-auto max-h-[600px]'; + slot.innerHTML = `
${data.content.replace(/\n/g, '
')}
`; + } else { + const imgUrl = data.url; slot.className = 'image-frame group relative animate-in zoom-in-95 duration-700 flex flex-col items-center justify-center overflow-hidden bg-white shadow-2xl transition-all hover:shadow-indigo-100/50'; slot.innerHTML = `
@@ -504,20 +569,8 @@ document.getElementById('submitBtn').onclick = async () => {
`; - lucide.createIcons(); - } catch (e) { - slot.className = 'image-frame relative bg-red-50/50 flex items-center justify-center rounded-[2.5rem] border border-red-100 p-6'; - if(e.message.includes('401') || e.message.includes('请先登录')) { - slot.innerHTML = `
登录已过期,请重新登录
`; - } else { - slot.innerHTML = `
渲染异常: ${e.message}
`; - } - } finally { - finishedCount++; - btnText.innerText = `AI 构思中 (${finishedCount}/${num})...`; - // 每次生成任务结束后,刷新一次积分显示 - if(currentMode === 'trial') checkAuth(); } + lucide.createIcons(); }; const tasks = Array.from({ length: num }, (_, i) => startTask(i)); @@ -585,3 +638,79 @@ document.getElementById('pwdForm')?.addEventListener('submit', async (e) => { showToast('网络连接失败', 'error'); } }); + +// 拍摄角度设置器弹窗控制 +function openVisualizerModal() { + const modal = document.getElementById('visualizerModal'); + if(!modal) return; + + // 检查是否已上传参考图 + if (uploadedFiles.length === 0) { + return showToast('请先上传一张参考图作为基准', 'warning'); + } + + modal.classList.remove('hidden'); + setTimeout(() => { + modal.classList.add('opacity-100'); + modal.querySelector('div').classList.remove('scale-95'); + + // 将主页面的图片同步到设置器 iframe + const reader = new FileReader(); + reader.onload = (e) => { + const iframe = document.getElementById('visualizerFrame'); + iframe.contentWindow.postMessage({ type: 'sync_image', dataUrl: e.target.result }, '*'); + }; + reader.readAsDataURL(uploadedFiles[0]); + }, 10); +} + +function closeVisualizerModal() { + const modal = document.getElementById('visualizerModal'); + if(!modal) return; + modal.classList.remove('opacity-100'); + modal.querySelector('div').classList.add('scale-95'); + setTimeout(() => { + modal.classList.add('hidden'); + }, 300); +} + +// 监听来自设置器的消息 +window.addEventListener('message', (e) => { + if (e.data.type === 'apply_prompt') { + isSetterActive = true; + + const area = document.getElementById('manualPrompt'); + const promptTpl = document.getElementById('promptTpl'); + const modelSelect = document.getElementById('modelSelect'); + const sizeGroup = document.getElementById('sizeGroup'); + + // 1. 强制切换并锁定版本 2.0 (nano-banana-2) + if (modelSelect) { + modelSelect.value = 'nano-banana-2'; + modelSelect.disabled = true; // 锁定选择 + sizeGroup.classList.remove('hidden'); + updateCostPreview(); + } + + // 2. 隐藏模板选择器 + if (promptTpl) { + promptTpl.classList.add('hidden'); + promptTpl.value = 'manual'; + } + + // 3. 强制图片数量为 1 + if (uploadedFiles.length > 1) { + uploadedFiles = uploadedFiles.slice(0, 1); + renderImagePreviews(); + } + + // 4. 替换提示词 + if (area) { + area.value = e.data.prompt; + area.classList.remove('hidden'); + showToast('已同步拍摄角度并切换至 2.0 引擎', 'success'); + } + + closeVisualizerModal(); + } +}); diff --git a/templates/index.html b/templates/index.html index f7c2b35..be9a345 100644 --- a/templates/index.html +++ b/templates/index.html @@ -51,9 +51,15 @@
-
- 02 -

渲染设定

+
+
+ 02 +

渲染设定

+
+
@@ -79,7 +85,7 @@ - +
@@ -226,6 +232,29 @@ + + + {% endblock %} {% block scripts %} diff --git a/templates/kongzhiqi.html b/templates/kongzhiqi.html new file mode 100644 index 0000000..744f460 --- /dev/null +++ b/templates/kongzhiqi.html @@ -0,0 +1,460 @@ + + + + + + AI 3D Camera Visualizer + + + + + +
+
+ + +
+ +
+
准备就绪
+
+ +
+ +
+
+ Azimuth (水平) + +
+ +
+ 正前右侧正后左侧 +
+
+ + +
+
+ Elevation (高度) + +
+ +
+ 仰拍平视俯拍 +
+
+ + +
+
+ Distance (距离) + 1.0 +
+ +
+ 特写标准远景 +
+
+ + +
+ +
+
...
+
+
+ + + + + \ No newline at end of file