mirror of
https://github.com/TheSmallHanCat/flow2api.git
synced 2026-05-08 23:06:22 +08:00
Merge pull request #108
This commit is contained in:
@@ -114,6 +114,15 @@ python main.py
|
||||
- **用户名**: `admin`
|
||||
- **密码**: `admin`
|
||||
|
||||
### 模型测试页面
|
||||
|
||||
访问 **http://localhost:8000/test** 可打开内置的模型测试页面,支持:
|
||||
|
||||
- 按分类浏览所有可用模型(图片生成、文/图生视频、多图视频、视频放大等)
|
||||
- 输入提示词一键测试,流式显示生成进度
|
||||
- 图生图 / 图生视频场景支持上传图片
|
||||
- 生成完成后直接预览图片或视频
|
||||
|
||||
## 📋 支持的模型
|
||||
|
||||
### 图片生成
|
||||
|
||||
@@ -225,3 +225,12 @@ async def manage_page():
|
||||
if manage_file.exists():
|
||||
return FileResponse(str(manage_file))
|
||||
return HTMLResponse(content="<h1>Management Page Not Found</h1>", status_code=404)
|
||||
|
||||
|
||||
@app.get("/test", response_class=HTMLResponse)
|
||||
async def test_page():
|
||||
"""Model testing page"""
|
||||
test_file = static_path / "test.html"
|
||||
if test_file.exists():
|
||||
return FileResponse(str(test_file))
|
||||
return HTMLResponse(content="<h1>Test Page Not Found</h1>", status_code=404)
|
||||
|
||||
514
static/test.html
Normal file
514
static/test.html
Normal file
@@ -0,0 +1,514 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Flow2API 模型测试</title>
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0f1117; color: #e1e1e6; min-height: 100vh; }
|
||||
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
|
||||
h1 { text-align: center; margin-bottom: 8px; font-size: 24px; color: #7c8aff; }
|
||||
.subtitle { text-align: center; color: #666; margin-bottom: 24px; font-size: 14px; }
|
||||
|
||||
.config-bar {
|
||||
display: flex; gap: 12px; margin-bottom: 20px; align-items: center;
|
||||
background: #1a1b26; padding: 12px 16px; border-radius: 10px; border: 1px solid #2a2b3a;
|
||||
}
|
||||
.config-bar label { font-size: 13px; color: #888; white-space: nowrap; }
|
||||
.config-bar input { flex: 1; background: #0f1117; border: 1px solid #2a2b3a; color: #e1e1e6; padding: 8px 12px; border-radius: 6px; font-size: 14px; }
|
||||
|
||||
.main-layout { display: flex; gap: 20px; }
|
||||
.sidebar { width: 280px; flex-shrink: 0; }
|
||||
.content { flex: 1; min-width: 0; }
|
||||
|
||||
.model-section { margin-bottom: 16px; }
|
||||
.section-title {
|
||||
font-size: 13px; font-weight: 600; color: #7c8aff; padding: 8px 12px;
|
||||
background: #1a1b26; border-radius: 8px 8px 0 0; border: 1px solid #2a2b3a; border-bottom: none;
|
||||
cursor: pointer; user-select: none; display: flex; justify-content: space-between; align-items: center;
|
||||
}
|
||||
.section-title:hover { background: #1e1f2e; }
|
||||
.section-title .arrow { transition: transform 0.2s; }
|
||||
.section-title.collapsed .arrow { transform: rotate(-90deg); }
|
||||
.model-list {
|
||||
background: #1a1b26; border: 1px solid #2a2b3a; border-radius: 0 0 8px 8px;
|
||||
max-height: 300px; overflow-y: auto;
|
||||
}
|
||||
.model-list.hidden { display: none; }
|
||||
.model-item {
|
||||
padding: 8px 12px; font-size: 13px; cursor: pointer; border-bottom: 1px solid #1e1f2e;
|
||||
display: flex; justify-content: space-between; align-items: center;
|
||||
}
|
||||
.model-item:last-child { border-bottom: none; }
|
||||
.model-item:hover { background: #1e1f2e; }
|
||||
.model-item.active { background: #2a2b5a; color: #7c8aff; }
|
||||
.model-tag {
|
||||
font-size: 10px; padding: 2px 6px; border-radius: 4px; font-weight: 500;
|
||||
}
|
||||
.tag-image { background: #1a3a2a; color: #4ade80; }
|
||||
.tag-video { background: #3a2a1a; color: #fb923c; }
|
||||
|
||||
.gen-panel {
|
||||
background: #1a1b26; border: 1px solid #2a2b3a; border-radius: 10px; padding: 20px;
|
||||
}
|
||||
.gen-panel h3 { font-size: 16px; margin-bottom: 4px; }
|
||||
.gen-panel .model-info { font-size: 12px; color: #666; margin-bottom: 16px; }
|
||||
|
||||
.form-group { margin-bottom: 14px; }
|
||||
.form-group label { display: block; font-size: 13px; color: #888; margin-bottom: 6px; }
|
||||
.form-group textarea {
|
||||
width: 100%; background: #0f1117; border: 1px solid #2a2b3a; color: #e1e1e6;
|
||||
padding: 10px 12px; border-radius: 8px; font-size: 14px; resize: vertical; min-height: 80px;
|
||||
font-family: inherit;
|
||||
}
|
||||
.form-group textarea:focus { outline: none; border-color: #7c8aff; }
|
||||
|
||||
.image-upload-area {
|
||||
border: 2px dashed #2a2b3a; border-radius: 8px; padding: 20px; text-align: center;
|
||||
cursor: pointer; transition: border-color 0.2s; position: relative;
|
||||
}
|
||||
.image-upload-area:hover { border-color: #7c8aff; }
|
||||
.image-upload-area.has-images { padding: 10px; }
|
||||
.image-upload-hint { color: #555; font-size: 13px; }
|
||||
.image-preview-list { display: flex; gap: 8px; flex-wrap: wrap; }
|
||||
.image-preview-item {
|
||||
position: relative; width: 80px; height: 80px; border-radius: 6px; overflow: hidden;
|
||||
}
|
||||
.image-preview-item img { width: 100%; height: 100%; object-fit: cover; }
|
||||
.image-preview-item .remove-btn {
|
||||
position: absolute; top: 2px; right: 2px; width: 20px; height: 20px;
|
||||
background: rgba(0,0,0,0.7); color: #fff; border: none; border-radius: 50%;
|
||||
cursor: pointer; font-size: 12px; display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
|
||||
.btn-generate {
|
||||
width: 100%; padding: 12px; background: #7c8aff; color: #fff; border: none;
|
||||
border-radius: 8px; font-size: 15px; font-weight: 600; cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
.btn-generate:hover { background: #6b79ee; }
|
||||
.btn-generate:disabled { background: #3a3b5a; cursor: not-allowed; }
|
||||
|
||||
.output-panel {
|
||||
background: #1a1b26; border: 1px solid #2a2b3a; border-radius: 10px; padding: 20px; margin-top: 16px;
|
||||
}
|
||||
.output-panel h3 { font-size: 15px; margin-bottom: 12px; display: flex; align-items: center; gap: 8px; }
|
||||
.status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
|
||||
.status-idle { background: #555; }
|
||||
.status-running { background: #facc15; animation: pulse 1s infinite; }
|
||||
.status-done { background: #4ade80; }
|
||||
.status-error { background: #f87171; }
|
||||
@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
|
||||
|
||||
.output-log {
|
||||
background: #0f1117; border-radius: 8px; padding: 12px; font-size: 13px;
|
||||
line-height: 1.6; max-height: 200px; overflow-y: auto; white-space: pre-wrap;
|
||||
word-break: break-all; color: #aaa;
|
||||
}
|
||||
.output-result { margin-top: 12px; }
|
||||
.output-result img { max-width: 100%; border-radius: 8px; margin-top: 8px; }
|
||||
.output-result video { max-width: 100%; border-radius: 8px; margin-top: 8px; }
|
||||
|
||||
.model-list::-webkit-scrollbar, .output-log::-webkit-scrollbar { width: 4px; }
|
||||
.model-list::-webkit-scrollbar-thumb, .output-log::-webkit-scrollbar-thumb { background: #333; border-radius: 2px; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.main-layout { flex-direction: column; }
|
||||
.sidebar { width: 100%; }
|
||||
.config-bar { flex-wrap: wrap; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🧪 Flow2API 模型测试</h1>
|
||||
<p class="subtitle">选择模型,输入提示词,测试生成效果</p>
|
||||
|
||||
<div class="config-bar">
|
||||
<label>API Key:</label>
|
||||
<input type="password" id="apiKey" value="han1234" placeholder="输入 API Key">
|
||||
<label>地址:</label>
|
||||
<input type="text" id="baseUrl" placeholder="http://localhost:8000" />
|
||||
</div>
|
||||
|
||||
<div class="main-layout">
|
||||
<div class="sidebar" id="modelSidebar"></div>
|
||||
<div class="content">
|
||||
<div class="gen-panel">
|
||||
<h3 id="selectedModelName">请选择模型</h3>
|
||||
<div class="model-info" id="selectedModelInfo">从左侧列表选择一个模型开始测试</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>提示词 (Prompt)</label>
|
||||
<textarea id="promptInput" placeholder="描述你想生成的内容...">一只可爱的橘猫趴在窗台上晒太阳,窗外是樱花盛开的春天</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group" id="imageUploadGroup" style="display:none;">
|
||||
<label>上传图片 <span id="imageLimit" style="color:#666;font-size:12px;"></span></label>
|
||||
<div class="image-upload-area" id="imageUploadArea">
|
||||
<div class="image-upload-hint" id="imageHint">点击或拖拽上传图片</div>
|
||||
<div class="image-preview-list" id="imagePreviewList"></div>
|
||||
</div>
|
||||
<input type="file" id="imageFileInput" accept="image/*" multiple style="display:none;">
|
||||
</div>
|
||||
|
||||
<button class="btn-generate" id="btnGenerate" disabled>选择模型后开始生成</button>
|
||||
</div>
|
||||
|
||||
<div class="output-panel">
|
||||
<h3><span class="status-dot status-idle" id="statusDot"></span> <span id="statusText">等待中</span></h3>
|
||||
<div class="output-log" id="outputLog">准备就绪,选择模型并点击生成按钮开始...</div>
|
||||
<div class="output-result" id="outputResult"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const STATE = { selectedModel: null, modelConfig: null, images: [], generating: false };
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<script>
|
||||
// Model categories for sidebar
|
||||
const MODEL_CATEGORIES = {
|
||||
"Gemini 3.1 Flash 图片": { filter: m => m.startsWith("gemini-3.1-flash-image") },
|
||||
"Gemini 3.0 Pro 图片": { filter: m => m.startsWith("gemini-3.0-pro-image") },
|
||||
"Gemini 2.5 Flash 图片": { filter: m => m.startsWith("gemini-2.5-flash-image") },
|
||||
"Imagen 4.0 图片": { filter: m => m.startsWith("imagen-4.0") },
|
||||
"Veo 3.1 文生视频 (T2V)": { filter: m => m.startsWith("veo_3_1_t2v") && !m.includes("4k") && !m.includes("1080p") },
|
||||
"Veo 3.1 图生视频 (I2V)": { filter: m => m.startsWith("veo_3_1_i2v") && !m.includes("4k") && !m.includes("1080p") },
|
||||
"Veo 3.1 多图视频 (R2V)": { filter: m => m.startsWith("veo_3_1_r2v") && !m.includes("4k") && !m.includes("1080p") },
|
||||
"Veo 2.x 视频": { filter: m => m.startsWith("veo_2") },
|
||||
"视频放大 (Upsample)": { filter: m => m.includes("4k") || m.includes("1080p") },
|
||||
};
|
||||
|
||||
let ALL_MODELS = {};
|
||||
|
||||
async function loadModels() {
|
||||
const baseUrl = document.getElementById("baseUrl").value || "";
|
||||
const apiKey = document.getElementById("apiKey").value;
|
||||
try {
|
||||
const resp = await fetch(`${baseUrl}/v1/models`, {
|
||||
headers: { "Authorization": `Bearer ${apiKey}` }
|
||||
});
|
||||
const data = await resp.json();
|
||||
ALL_MODELS = {};
|
||||
(data.data || []).forEach(m => { ALL_MODELS[m.id] = m.description; });
|
||||
renderSidebar();
|
||||
} catch (e) {
|
||||
console.error("加载模型失败:", e);
|
||||
renderSidebarFallback();
|
||||
}
|
||||
}
|
||||
|
||||
function renderSidebarFallback() {
|
||||
// Use hardcoded categories if API fails
|
||||
renderSidebar();
|
||||
}
|
||||
|
||||
function getModelType(modelId) {
|
||||
if (modelId.includes("image") || modelId.startsWith("imagen")) return "image";
|
||||
return "video";
|
||||
}
|
||||
|
||||
function getModelMeta(modelId) {
|
||||
// Determine supports_images, min/max from model name patterns
|
||||
const isI2V = modelId.includes("i2v");
|
||||
const isR2V = modelId.includes("r2v");
|
||||
const isT2V = modelId.includes("t2v");
|
||||
const isImage = getModelType(modelId) === "image";
|
||||
|
||||
if (isImage) return { type: "image", supportsImages: true, minImages: 0, maxImages: 5 };
|
||||
if (isI2V) return { type: "video", supportsImages: true, minImages: 1, maxImages: 2 };
|
||||
if (isR2V) return { type: "video", supportsImages: true, minImages: 0, maxImages: 3 };
|
||||
return { type: "video", supportsImages: false, minImages: 0, maxImages: 0 };
|
||||
}
|
||||
|
||||
function renderSidebar() {
|
||||
const sidebar = document.getElementById("modelSidebar");
|
||||
sidebar.innerHTML = "";
|
||||
|
||||
const modelIds = Object.keys(ALL_MODELS);
|
||||
|
||||
for (const [catName, catDef] of Object.entries(MODEL_CATEGORIES)) {
|
||||
const models = modelIds.filter(catDef.filter);
|
||||
if (models.length === 0) continue;
|
||||
|
||||
const section = document.createElement("div");
|
||||
section.className = "model-section";
|
||||
|
||||
const title = document.createElement("div");
|
||||
title.className = "section-title";
|
||||
title.innerHTML = `<span>${catName} (${models.length})</span><span class="arrow">▼</span>`;
|
||||
|
||||
const list = document.createElement("div");
|
||||
list.className = "model-list";
|
||||
|
||||
models.forEach(modelId => {
|
||||
const item = document.createElement("div");
|
||||
item.className = "model-item";
|
||||
item.dataset.model = modelId;
|
||||
|
||||
const type = getModelType(modelId);
|
||||
const shortName = modelId.replace("gemini-3.1-flash-image-", "").replace("gemini-3.0-pro-image-", "").replace("gemini-2.5-flash-image-", "").replace("imagen-4.0-generate-preview-", "");
|
||||
item.innerHTML = `<span>${shortName}</span><span class="model-tag ${type === 'image' ? 'tag-image' : 'tag-video'}">${type === 'image' ? '图片' : '视频'}</span>`;
|
||||
item.onclick = () => selectModel(modelId);
|
||||
list.appendChild(item);
|
||||
});
|
||||
|
||||
title.onclick = () => {
|
||||
title.classList.toggle("collapsed");
|
||||
list.classList.toggle("hidden");
|
||||
};
|
||||
|
||||
section.appendChild(title);
|
||||
section.appendChild(list);
|
||||
sidebar.appendChild(section);
|
||||
}
|
||||
}
|
||||
|
||||
function selectModel(modelId) {
|
||||
STATE.selectedModel = modelId;
|
||||
STATE.modelConfig = getModelMeta(modelId);
|
||||
STATE.images = [];
|
||||
|
||||
// Update sidebar active state
|
||||
document.querySelectorAll(".model-item").forEach(el => {
|
||||
el.classList.toggle("active", el.dataset.model === modelId);
|
||||
});
|
||||
|
||||
// Update panel
|
||||
document.getElementById("selectedModelName").textContent = modelId;
|
||||
document.getElementById("selectedModelInfo").textContent = ALL_MODELS[modelId] || "";
|
||||
|
||||
// Show/hide image upload
|
||||
const uploadGroup = document.getElementById("imageUploadGroup");
|
||||
const meta = STATE.modelConfig;
|
||||
if (meta.supportsImages) {
|
||||
uploadGroup.style.display = "block";
|
||||
const limitText = meta.type === "image" ? "可选,用于图生图" : `${meta.minImages}-${meta.maxImages}张`;
|
||||
document.getElementById("imageLimit").textContent = `(${limitText})`;
|
||||
} else {
|
||||
uploadGroup.style.display = "none";
|
||||
}
|
||||
|
||||
// Reset image previews
|
||||
document.getElementById("imagePreviewList").innerHTML = "";
|
||||
document.getElementById("imageHint").style.display = "block";
|
||||
|
||||
// Enable button
|
||||
const btn = document.getElementById("btnGenerate");
|
||||
btn.disabled = false;
|
||||
btn.textContent = meta.type === "image" ? "🎨 生成图片" : "🎬 生成视频";
|
||||
}
|
||||
|
||||
// Image upload handling
|
||||
const uploadArea = document.getElementById("imageUploadArea");
|
||||
const fileInput = document.getElementById("imageFileInput");
|
||||
|
||||
uploadArea.onclick = (e) => {
|
||||
if (e.target.closest(".remove-btn")) return;
|
||||
fileInput.click();
|
||||
};
|
||||
|
||||
uploadArea.ondragover = (e) => { e.preventDefault(); uploadArea.style.borderColor = "#7c8aff"; };
|
||||
uploadArea.ondragleave = () => { uploadArea.style.borderColor = "#2a2b3a"; };
|
||||
uploadArea.ondrop = (e) => {
|
||||
e.preventDefault();
|
||||
uploadArea.style.borderColor = "#2a2b3a";
|
||||
handleFiles(e.dataTransfer.files);
|
||||
};
|
||||
|
||||
fileInput.onchange = () => { handleFiles(fileInput.files); fileInput.value = ""; };
|
||||
|
||||
function handleFiles(files) {
|
||||
const maxImages = STATE.modelConfig ? STATE.modelConfig.maxImages : 5;
|
||||
for (const file of files) {
|
||||
if (STATE.images.length >= maxImages) break;
|
||||
if (!file.type.startsWith("image/")) continue;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
STATE.images.push(e.target.result);
|
||||
renderImagePreviews();
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
|
||||
function renderImagePreviews() {
|
||||
const list = document.getElementById("imagePreviewList");
|
||||
const hint = document.getElementById("imageHint");
|
||||
list.innerHTML = "";
|
||||
hint.style.display = STATE.images.length ? "none" : "block";
|
||||
|
||||
STATE.images.forEach((dataUrl, idx) => {
|
||||
const item = document.createElement("div");
|
||||
item.className = "image-preview-item";
|
||||
item.innerHTML = `<img src="${dataUrl}" alt="preview"><button class="remove-btn" onclick="removeImage(${idx})">×</button>`;
|
||||
list.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
function removeImage(idx) {
|
||||
STATE.images.splice(idx, 1);
|
||||
renderImagePreviews();
|
||||
}
|
||||
|
||||
// Generation
|
||||
document.getElementById("btnGenerate").onclick = generate;
|
||||
|
||||
async function generate() {
|
||||
if (!STATE.selectedModel || STATE.generating) return;
|
||||
|
||||
const baseUrl = document.getElementById("baseUrl").value || "";
|
||||
const apiKey = document.getElementById("apiKey").value;
|
||||
const prompt = document.getElementById("promptInput").value.trim();
|
||||
if (!prompt) { alert("请输入提示词"); return; }
|
||||
|
||||
STATE.generating = true;
|
||||
const btn = document.getElementById("btnGenerate");
|
||||
btn.disabled = true;
|
||||
btn.textContent = "⏳ 生成中...";
|
||||
|
||||
const dot = document.getElementById("statusDot");
|
||||
dot.className = "status-dot status-running";
|
||||
document.getElementById("statusText").textContent = "生成中...";
|
||||
document.getElementById("outputLog").textContent = "";
|
||||
document.getElementById("outputResult").innerHTML = "";
|
||||
|
||||
const log = document.getElementById("outputLog");
|
||||
const appendLog = (text) => { log.textContent += text; log.scrollTop = log.scrollHeight; };
|
||||
|
||||
// Build messages
|
||||
const content = [];
|
||||
content.push({ type: "text", text: prompt });
|
||||
STATE.images.forEach(dataUrl => {
|
||||
content.push({ type: "image_url", image_url: { url: dataUrl } });
|
||||
});
|
||||
|
||||
const messages = [{ role: "user", content: content.length === 1 ? prompt : content }];
|
||||
|
||||
const body = { model: STATE.selectedModel, messages, stream: true };
|
||||
|
||||
const startTime = Date.now();
|
||||
appendLog(`[${new Date().toLocaleTimeString()}] 模型: ${STATE.selectedModel}\n`);
|
||||
appendLog(`[${new Date().toLocaleTimeString()}] 提示词: ${prompt.substring(0, 100)}${prompt.length > 100 ? '...' : ''}\n`);
|
||||
appendLog(`[${new Date().toLocaleTimeString()}] 开始请求...\n`);
|
||||
|
||||
try {
|
||||
const resp = await fetch(`${baseUrl}/v1/chat/completions`, {
|
||||
method: "POST",
|
||||
headers: { "Authorization": `Bearer ${apiKey}`, "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
if (!resp.ok) {
|
||||
const errText = await resp.text();
|
||||
throw new Error(`HTTP ${resp.status}: ${errText}`);
|
||||
}
|
||||
|
||||
const reader = resp.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let fullContent = "";
|
||||
let buffer = "";
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
const lines = buffer.split("\n");
|
||||
buffer = lines.pop() || "";
|
||||
|
||||
for (const line of lines) {
|
||||
if (!line.startsWith("data: ")) continue;
|
||||
const data = line.slice(6).trim();
|
||||
if (data === "[DONE]") continue;
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(data);
|
||||
if (parsed.error) {
|
||||
appendLog(`\n❌ 错误: ${JSON.stringify(parsed.error)}\n`);
|
||||
continue;
|
||||
}
|
||||
const choices = parsed.choices || [];
|
||||
if (choices.length > 0) {
|
||||
const delta = choices[0].delta || {};
|
||||
const content = delta.reasoning_content || delta.content || "";
|
||||
if (content) {
|
||||
fullContent += content;
|
||||
appendLog(content);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// skip unparseable chunks
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
|
||||
appendLog(`\n\n[${new Date().toLocaleTimeString()}] 完成,耗时 ${elapsed}s\n`);
|
||||
|
||||
// Render result
|
||||
renderResult(fullContent);
|
||||
dot.className = "status-dot status-done";
|
||||
document.getElementById("statusText").textContent = `完成 (${elapsed}s)`;
|
||||
|
||||
} catch (e) {
|
||||
appendLog(`\n❌ 请求失败: ${e.message}\n`);
|
||||
dot.className = "status-dot status-error";
|
||||
document.getElementById("statusText").textContent = "失败";
|
||||
} finally {
|
||||
STATE.generating = false;
|
||||
btn.disabled = false;
|
||||
const meta = STATE.modelConfig;
|
||||
btn.textContent = meta && meta.type === "image" ? "🎨 生成图片" : "🎬 生成视频";
|
||||
}
|
||||
}
|
||||
|
||||
function renderResult(content) {
|
||||
const resultDiv = document.getElementById("outputResult");
|
||||
resultDiv.innerHTML = "";
|
||||
|
||||
// Extract markdown images
|
||||
const imgRegex = /!\[.*?\]\((.*?)\)/g;
|
||||
let match;
|
||||
while ((match = imgRegex.exec(content)) !== null) {
|
||||
const img = document.createElement("img");
|
||||
img.src = match[1];
|
||||
img.alt = "生成结果";
|
||||
img.loading = "lazy";
|
||||
resultDiv.appendChild(img);
|
||||
}
|
||||
|
||||
// Extract video tags
|
||||
const videoRegex = /<video[^>]+src=['"]([^'"]+)['"]/gi;
|
||||
while ((match = videoRegex.exec(content)) !== null) {
|
||||
const video = document.createElement("video");
|
||||
video.src = match[1];
|
||||
video.controls = true;
|
||||
video.autoplay = true;
|
||||
video.loop = true;
|
||||
resultDiv.appendChild(video);
|
||||
}
|
||||
|
||||
// If no media found, show raw text
|
||||
if (resultDiv.children.length === 0 && content.trim()) {
|
||||
const pre = document.createElement("pre");
|
||||
pre.style.cssText = "color:#aaa;font-size:13px;white-space:pre-wrap;word-break:break-all;";
|
||||
pre.textContent = content;
|
||||
resultDiv.appendChild(pre);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-detect base URL
|
||||
(function() {
|
||||
const input = document.getElementById("baseUrl");
|
||||
input.value = window.location.origin;
|
||||
loadModels();
|
||||
})();
|
||||
</script>
|
||||
Reference in New Issue
Block a user