save code
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Running the Services
|
||||
|
||||
Each service runs independently and must be started separately:
|
||||
|
||||
```bash
|
||||
# Shirt-only try-on service (port 8888)
|
||||
python change2/service2.py
|
||||
|
||||
# Shirt + pants try-on service (port 8889)
|
||||
python change3/service3.py
|
||||
|
||||
# Gateway router (port 28888) — requires Flask
|
||||
python change_app.py
|
||||
```
|
||||
|
||||
ComfyUI must be running at `http://127.0.0.1:8188` before starting the services.
|
||||
|
||||
## Dependencies
|
||||
|
||||
```bash
|
||||
pip install fastapi uvicorn httpx pydantic flask requests
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
This is a three-tier AI virtual try-on system built around ComfyUI.
|
||||
|
||||
```
|
||||
Client
|
||||
└─► change_app.py (Flask, port 28888) ← Gateway router
|
||||
├─► change2/service2.py (FastAPI, port 8888) ← shirt-only
|
||||
└─► change3/service3.py (FastAPI, port 8889) ← shirt + pants
|
||||
└─► ComfyUI (port 8188)
|
||||
```
|
||||
|
||||
**Gateway** (`change_app.py`): Accepts `POST /change_cloth_base64` with `human_img`, `cloth_img`, and optional `kuzi_img` (pants). Routes to `service3` when `kuzi_img` is present, otherwise to `service2`. Both backends are called at `/do_change_cloth`.
|
||||
|
||||
**service2** (`change2/service2.py`): Handles shirt-only try-on. Exposes `POST /try-on` with `model_image` + `shirt_image`. Uses ComfyUI workflow `change2_1_ex.json`.
|
||||
|
||||
**service3** (`change3/service3.py`): Handles shirt + pants try-on. Exposes `POST /try-on` with `model_image` + `shirt_image` + `pants_image`. Uses ComfyUI workflow `change2_2_0308.json`.
|
||||
|
||||
### ComfyUI Workflow Integration
|
||||
|
||||
Both services dynamically modify a JSON workflow template before submission:
|
||||
|
||||
| Node ID | Purpose | service2 | service3 |
|
||||
|---------|---------|----------|----------|
|
||||
| `"11"` | Model/person image | ✓ | ✓ |
|
||||
| `"10"` | Shirt image | ✓ | ✓ |
|
||||
| `"4"` | Pants image | — | ✓ |
|
||||
| `"33"` | Output image | ✓ | ✓ |
|
||||
|
||||
The request flow within each service:
|
||||
1. Decode base64 images
|
||||
2. Upload images to ComfyUI (`/upload/image`) with UUID-prefixed filenames to avoid cache collisions
|
||||
3. Inject filenames into workflow node inputs
|
||||
4. Submit workflow to ComfyUI queue (`/prompt`)
|
||||
5. Poll `/history/{prompt_id}` until `status.completed` is true (max 300s timeout, 2s poll interval)
|
||||
6. Download result from `/view` and return as base64
|
||||
|
||||
### Frontend
|
||||
|
||||
Each service has its own test UI at `/` (served from `<service_dir>/static/index.html`). The `change2` UI accepts 2 images; `change3` accepts 3 images. Both POST directly to the service's `/try-on` endpoint.
|
||||
+1
-1
@@ -198,4 +198,4 @@ async def index():
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=47698, log_level="info")
|
||||
uvicorn.run(app, host="0.0.0.0", port=12222, log_level="info")
|
||||
|
||||
+1
-1
@@ -200,4 +200,4 @@ async def index():
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(app, host="0.0.0.0", port=47698, log_level="info")
|
||||
uvicorn.run(app, host="0.0.0.0", port=12223, log_level="info")
|
||||
|
||||
+57
-63
@@ -1,77 +1,71 @@
|
||||
import requests
|
||||
from flask import Flask, jsonify, request, send_from_directory
|
||||
|
||||
app = Flask(__name__, static_folder='static')
|
||||
|
||||
|
||||
@app.route('/')
|
||||
def index():
|
||||
return send_from_directory(app.static_folder, 'index.html')
|
||||
|
||||
|
||||
@app.route('/static/<path:filename>')
|
||||
def static_files(filename):
|
||||
return send_from_directory(app.static_folder, filename)
|
||||
|
||||
|
||||
@app.route('/change_cloth_base64', methods=['POST'])
|
||||
def change_cloth_base64():
|
||||
log_message("change_cloth_base64 called")
|
||||
|
||||
# 获取参数
|
||||
data = request.get_json()
|
||||
print(f"change_cloth_base64 input data:{data}")
|
||||
print(f"change_cloth_base64 input data keys:{list(data.keys()) if data else None}")
|
||||
if not data:
|
||||
return jsonify({"ret":-1, "state":-1, 'msg': 'No JSON data provided'}), 400
|
||||
return jsonify({"ret": -1, "state": -1, "msg": "No JSON data provided"}), 400
|
||||
|
||||
human_img = data.get('human_img')
|
||||
cloth_img = data.get('cloth_img')
|
||||
output_format = data.get('output_format')
|
||||
if not output_format:
|
||||
return jsonify({"state":-1, "error": "Missing 'output_format' parameter"}), 500
|
||||
|
||||
|
||||
if not human_img or not cloth_img:
|
||||
return jsonify({"ret":-1, 'msg': 'Both human_img and cloth_img are required'}), 400
|
||||
|
||||
human_filename = save_base64_image(human_img, 'human')
|
||||
if not human_filename:
|
||||
return jsonify({"ret":-1, 'msg': 'Failed to save human image'}), 500
|
||||
data['human_img'] = None
|
||||
|
||||
human_url = f"http://117.50.44.174:{base64_test_port}/static/imgs/{human_filename}"
|
||||
|
||||
# 保存服装图片
|
||||
cloth_filename = save_base64_image(cloth_img, 'cloth')
|
||||
if not cloth_filename:
|
||||
return jsonify({"ret":-1, 'msg': 'Failed to save cloth image'}), 500
|
||||
data['cloth_img'] = None
|
||||
|
||||
cloth_url = f"http://117.50.44.174:{base64_test_port}/static/imgs/{cloth_filename}"
|
||||
|
||||
data["human_url"] = human_url
|
||||
data["cloth_url"] = cloth_url
|
||||
return jsonify({"ret": -1, "state": -1, "msg": "Both human_img and cloth_img are required"}), 400
|
||||
|
||||
kuzi_img = data.get('kuzi_img')
|
||||
if kuzi_img:
|
||||
kuzi_filename = save_base64_image(kuzi_img, 'kuzi')
|
||||
# data['kuzi_img'] = None
|
||||
kuzi_url = f"http://117.50.44.174:{base64_test_port}/static/imgs/{kuzi_filename}"
|
||||
data['kuzi_url'] = kuzi_url
|
||||
|
||||
no2 = data.get('no2')
|
||||
if not no2:
|
||||
data['no2'] = False
|
||||
|
||||
tuodi = data.get('tuodi')
|
||||
if not tuodi:
|
||||
data['tuodi'] = False
|
||||
|
||||
|
||||
if not data.get('suit'):
|
||||
data['suit'] = False
|
||||
|
||||
if data.get('cloth_len'):
|
||||
print(f'客户端传入了衣服长度: {data['cloth_len']}')
|
||||
else:
|
||||
print('客户端传入了衣服长度,需要ai 判断')
|
||||
|
||||
|
||||
# 在内部调用第二个HTTP请求
|
||||
try:
|
||||
# 调用第二个API(可以是外部服务或自己的另一个端点)
|
||||
response = requests.post(f'http://127.0.0.1:{base64_test_port}/do_change_cloth', json=data)
|
||||
|
||||
return Response(
|
||||
response=response.content,
|
||||
status=response.status_code,
|
||||
headers=dict(response.headers)
|
||||
)
|
||||
except requests.exceptions.RequestException as e:
|
||||
data['second_api_error'] = str(e)
|
||||
|
||||
return jsonify("error"), 500
|
||||
if kuzi_img:
|
||||
payload = {
|
||||
'model_image': human_img,
|
||||
'shirt_image': cloth_img,
|
||||
'pants_image': kuzi_img,
|
||||
}
|
||||
response = requests.post('http://127.0.0.1:12223/try-on', json=payload, timeout=360)
|
||||
else:
|
||||
payload = {
|
||||
'model_image': human_img,
|
||||
'shirt_image': cloth_img,
|
||||
}
|
||||
response = requests.post('http://127.0.0.1:12222/try-on', json=payload, timeout=360)
|
||||
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
result_image = result.get('result_image')
|
||||
|
||||
if not result_image:
|
||||
return jsonify({"ret": -1, "state": -1, "msg": "Backend returned no image"}), 500
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
return jsonify({"ret": -1, "state": -1, "msg": "Backend service timeout"}), 504
|
||||
except requests.exceptions.ConnectionError:
|
||||
return jsonify({"ret": -1, "state": -1, "msg": "Cannot connect to backend service"}), 502
|
||||
except requests.exceptions.HTTPError as e:
|
||||
return jsonify({"ret": -1, "state": -1, "msg": f"Backend error: {e}"}), 502
|
||||
|
||||
return jsonify({
|
||||
"ret": 0,
|
||||
"state": 0,
|
||||
"msg": "success",
|
||||
"data": result_image,
|
||||
})
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(host="0.0.0.0", port=28888, debug=False)
|
||||
|
||||
@@ -0,0 +1,377 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AI 换装测试</title>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
body {
|
||||
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
background: #0f0f13;
|
||||
color: #e0e0e0;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 32px 16px 64px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
background: linear-gradient(135deg, #a78bfa, #60a5fa, #34d399);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin-bottom: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 0.9rem;
|
||||
color: #888;
|
||||
margin-bottom: 40px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.upload-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 20px;
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.upload-card {
|
||||
background: #1a1a24;
|
||||
border: 2px dashed #333;
|
||||
border-radius: 16px;
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s, background 0.2s;
|
||||
position: relative;
|
||||
min-height: 280px;
|
||||
}
|
||||
|
||||
.upload-card:hover { border-color: #7c3aed; background: #1e1e2e; }
|
||||
.upload-card.has-image { border-style: solid; border-color: #6d28d9; }
|
||||
.upload-card.optional { border-color: #2a2a3a; }
|
||||
.upload-card.optional .upload-label { color: #8b80b0; }
|
||||
|
||||
.upload-card input[type="file"] {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.upload-icon { font-size: 2.5rem; line-height: 1; }
|
||||
.upload-label { font-size: 1rem; font-weight: 600; color: #c4b5fd; }
|
||||
.upload-hint { font-size: 0.78rem; color: #666; text-align: center; }
|
||||
|
||||
.badge-optional {
|
||||
font-size: 0.68rem;
|
||||
background: #2a2a3a;
|
||||
color: #777;
|
||||
border-radius: 6px;
|
||||
padding: 2px 8px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.preview-img {
|
||||
width: 100%;
|
||||
max-height: 200px;
|
||||
object-fit: contain;
|
||||
border-radius: 10px;
|
||||
display: none;
|
||||
}
|
||||
.preview-img.visible { display: block; }
|
||||
|
||||
.btn-clear {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: #3a1a1a;
|
||||
border: none;
|
||||
color: #f87171;
|
||||
border-radius: 50%;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 0.8rem;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1;
|
||||
}
|
||||
.btn-clear.visible { display: flex; }
|
||||
|
||||
.btn-run {
|
||||
padding: 14px 56px;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
border: none;
|
||||
border-radius: 50px;
|
||||
background: linear-gradient(135deg, #7c3aed, #2563eb);
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s, transform 0.1s;
|
||||
}
|
||||
.btn-run:hover:not(:disabled) { opacity: 0.9; transform: translateY(-1px); }
|
||||
.btn-run:disabled { opacity: 0.45; cursor: not-allowed; }
|
||||
|
||||
.status-bar {
|
||||
margin-top: 20px;
|
||||
font-size: 0.9rem;
|
||||
color: #888;
|
||||
min-height: 24px;
|
||||
text-align: center;
|
||||
}
|
||||
.status-bar.running { color: #60a5fa; }
|
||||
.status-bar.success { color: #34d399; }
|
||||
.status-bar.error { color: #f87171; }
|
||||
|
||||
.spinner {
|
||||
display: inline-block;
|
||||
width: 14px; height: 14px;
|
||||
border: 2px solid #60a5fa44;
|
||||
border-top-color: #60a5fa;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
vertical-align: middle;
|
||||
margin-right: 6px;
|
||||
}
|
||||
@keyframes spin { to { transform: rotate(360deg); } }
|
||||
|
||||
.result-section {
|
||||
margin-top: 40px;
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
display: none;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
.result-section.visible { display: flex; }
|
||||
|
||||
.result-title { font-size: 1.1rem; font-weight: 600; color: #a78bfa; }
|
||||
|
||||
.result-img {
|
||||
width: 100%;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 0 40px #7c3aed44;
|
||||
}
|
||||
|
||||
.btn-download {
|
||||
padding: 10px 36px;
|
||||
border: 2px solid #7c3aed;
|
||||
border-radius: 50px;
|
||||
background: transparent;
|
||||
color: #a78bfa;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.btn-download:hover { background: #7c3aed22; }
|
||||
|
||||
.raw-resp {
|
||||
width: 100%;
|
||||
max-width: 960px;
|
||||
margin-top: 32px;
|
||||
background: #1a1a24;
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
font-size: 0.78rem;
|
||||
color: #888;
|
||||
word-break: break-all;
|
||||
display: none;
|
||||
}
|
||||
.raw-resp.visible { display: block; }
|
||||
.raw-resp pre { white-space: pre-wrap; }
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.upload-grid { grid-template-columns: 1fr; }
|
||||
h1 { font-size: 1.5rem; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>AI 换装测试</h1>
|
||||
<p class="subtitle">上传模特和衣服图片,调用 /change_cloth_base64 接口测试</p>
|
||||
|
||||
<div class="upload-grid">
|
||||
<!-- 模特 -->
|
||||
<div class="upload-card" id="card-human">
|
||||
<button class="btn-clear" id="clear-human" title="清除">✕</button>
|
||||
<input type="file" accept="image/*" id="input-human" />
|
||||
<div class="upload-icon">🧍</div>
|
||||
<div class="upload-label">模特图片</div>
|
||||
<div class="upload-hint">human_img · 必填</div>
|
||||
<img class="preview-img" id="preview-human" alt="模特预览" />
|
||||
</div>
|
||||
|
||||
<!-- 上衣 -->
|
||||
<div class="upload-card" id="card-cloth">
|
||||
<button class="btn-clear" id="clear-cloth" title="清除">✕</button>
|
||||
<input type="file" accept="image/*" id="input-cloth" />
|
||||
<div class="upload-icon">👕</div>
|
||||
<div class="upload-label">上衣图片</div>
|
||||
<div class="upload-hint">cloth_img · 必填</div>
|
||||
<img class="preview-img" id="preview-cloth" alt="上衣预览" />
|
||||
</div>
|
||||
|
||||
<!-- 裤子(可选) -->
|
||||
<div class="upload-card optional" id="card-kuzi">
|
||||
<button class="btn-clear" id="clear-kuzi" title="清除">✕</button>
|
||||
<input type="file" accept="image/*" id="input-kuzi" />
|
||||
<div class="upload-icon">👖</div>
|
||||
<div class="upload-label">裤子图片</div>
|
||||
<div class="upload-hint">kuzi_img · 可选</div>
|
||||
<span class="badge-optional">有则走 8888 服务,无则走 8889</span>
|
||||
<img class="preview-img" id="preview-kuzi" alt="裤子预览" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button class="btn-run" id="btn-run" disabled>开始换装</button>
|
||||
<div class="status-bar" id="status-bar"></div>
|
||||
|
||||
<div class="result-section" id="result-section">
|
||||
<div class="result-title">换装结果</div>
|
||||
<img class="result-img" id="result-img" alt="换装结果" />
|
||||
<button class="btn-download" id="btn-download">下载图片</button>
|
||||
</div>
|
||||
|
||||
<div class="raw-resp" id="raw-resp">
|
||||
<div style="color:#555;margin-bottom:8px;font-size:0.72rem;">接口原始响应(data 字段已截断)</div>
|
||||
<pre id="raw-text"></pre>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const images = { human: null, cloth: null, kuzi: null };
|
||||
|
||||
function setupUpload(inputId, previewId, cardId, clearId, key) {
|
||||
const input = document.getElementById(inputId);
|
||||
const preview = document.getElementById(previewId);
|
||||
const card = document.getElementById(cardId);
|
||||
const clearBtn= document.getElementById(clearId);
|
||||
|
||||
input.addEventListener('change', (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (!file) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (ev) => {
|
||||
images[key] = ev.target.result;
|
||||
preview.src = ev.target.result;
|
||||
preview.classList.add('visible');
|
||||
card.classList.add('has-image');
|
||||
clearBtn.classList.add('visible');
|
||||
updateRunButton();
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
|
||||
clearBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
images[key] = null;
|
||||
input.value = '';
|
||||
preview.src = '';
|
||||
preview.classList.remove('visible');
|
||||
card.classList.remove('has-image');
|
||||
clearBtn.classList.remove('visible');
|
||||
updateRunButton();
|
||||
});
|
||||
}
|
||||
|
||||
setupUpload('input-human', 'preview-human', 'card-human', 'clear-human', 'human');
|
||||
setupUpload('input-cloth', 'preview-cloth', 'card-cloth', 'clear-cloth', 'cloth');
|
||||
setupUpload('input-kuzi', 'preview-kuzi', 'card-kuzi', 'clear-kuzi', 'kuzi');
|
||||
|
||||
function updateRunButton() {
|
||||
document.getElementById('btn-run').disabled = !(images.human && images.cloth);
|
||||
}
|
||||
|
||||
function setStatus(msg, cls = '') {
|
||||
const el = document.getElementById('status-bar');
|
||||
el.className = 'status-bar ' + cls;
|
||||
el.innerHTML = msg;
|
||||
}
|
||||
|
||||
function elapsed(start) {
|
||||
return Math.round((Date.now() - start) / 1000) + '秒';
|
||||
}
|
||||
|
||||
document.getElementById('btn-run').addEventListener('click', async () => {
|
||||
const btn = document.getElementById('btn-run');
|
||||
const resultSection = document.getElementById('result-section');
|
||||
const resultImg = document.getElementById('result-img');
|
||||
const rawResp = document.getElementById('raw-resp');
|
||||
const rawText = document.getElementById('raw-text');
|
||||
|
||||
btn.disabled = true;
|
||||
resultSection.classList.remove('visible');
|
||||
rawResp.classList.remove('visible');
|
||||
|
||||
const start = Date.now();
|
||||
let timer = setInterval(() => {
|
||||
setStatus(`<span class="spinner"></span>正在换装中,请稍候… 已等待 ${elapsed(start)}`, 'running');
|
||||
}, 1000);
|
||||
setStatus('<span class="spinner"></span>正在提交请求…', 'running');
|
||||
|
||||
const body = {
|
||||
human_img: images.human,
|
||||
cloth_img: images.cloth,
|
||||
};
|
||||
if (images.kuzi) body.kuzi_img = images.kuzi;
|
||||
|
||||
try {
|
||||
const resp = await fetch('/change_cloth_base64', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
clearInterval(timer);
|
||||
const data = await resp.json();
|
||||
|
||||
// 显示原始响应(data 字段截断)
|
||||
const preview = { ...data };
|
||||
if (preview.data && preview.data.length > 80) {
|
||||
preview.data = preview.data.slice(0, 80) + '…(已截断)';
|
||||
}
|
||||
rawText.textContent = JSON.stringify(preview, null, 2);
|
||||
rawResp.classList.add('visible');
|
||||
|
||||
if (data.ret !== 0 || !data.data) {
|
||||
throw new Error(data.msg || '接口返回失败');
|
||||
}
|
||||
|
||||
resultImg.src = data.data;
|
||||
resultSection.classList.add('visible');
|
||||
setStatus(`✅ 换装完成!耗时 ${elapsed(start)}`, 'success');
|
||||
|
||||
document.getElementById('btn-download').onclick = () => {
|
||||
const a = document.createElement('a');
|
||||
a.href = data.data;
|
||||
a.download = `tryon_${Date.now()}.png`;
|
||||
a.click();
|
||||
};
|
||||
|
||||
} catch (err) {
|
||||
clearInterval(timer);
|
||||
setStatus(`❌ 错误:${err.message}`, 'error');
|
||||
} finally {
|
||||
updateRunButton();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user