233 lines
9.6 KiB
JavaScript
233 lines
9.6 KiB
JavaScript
/* ============================================================
|
||
* debug_dashscope_image_models.js —— 探测百炼(DashScope)可用的文生图模型
|
||
*
|
||
* 目的:项目此前只用了 chat/completions 与 TTS,没有文生图调用。
|
||
* 本脚本枚举候选模型与候选端点,找出「本账号实际能用」的组合,
|
||
* 并把生成的图落盘到临时目录做 MD5 比对,确认不同 prompt 出的是不同的图。
|
||
*
|
||
* 运行:node dev_test_scripts/debug/debug_dashscope_image_models.js
|
||
* 说明:只读探测,不写入仓库任何文件(产物落在系统临时目录)。
|
||
* ============================================================ */
|
||
const fs = require('fs')
|
||
const path = require('path')
|
||
const crypto = require('crypto')
|
||
|
||
// 载入本机私有凭据(与项目其他模块同一份文件,不进仓库)
|
||
const envPath = path.join(process.env.USERPROFILE || 'C:\\Users\\Administrator', 'Toolbox_local_creds.env.local')
|
||
if (fs.existsSync(envPath)) {
|
||
const text = fs.readFileSync(envPath, 'utf8')
|
||
for (const line of text.split(/\r?\n/)) {
|
||
if (!line || /^\s*#/.test(line)) continue
|
||
const i = line.indexOf('=')
|
||
if (i < 0) continue
|
||
const k = line.slice(0, i).trim()
|
||
const v = line.slice(i + 1).trim().replace(/^["']|["']$/g, '')
|
||
if (k && !(k in process.env)) process.env[k] = v
|
||
}
|
||
}
|
||
|
||
const KEY = process.env.DASHSCOPE_API_KEY || ''
|
||
const MaaS = (process.env.DASHSCOPE_API_HOST_FOR_OPENAI || '').replace(/\/+$/, '')
|
||
const PUBLIC_BASE = 'https://dashscope.aliyuncs.com'
|
||
|
||
const OUT = path.join(process.env.TEMP || 'C:\\Windows\\Temp', 'tc_img_probe')
|
||
fs.mkdirSync(OUT, { recursive: true })
|
||
|
||
const promptA = 'coastal embankment above tidal flat, wind turbines, autumn afternoon'
|
||
const promptB = 'old Chinese town stone lane with grey brick houses and red lanterns'
|
||
|
||
console.log('[env] KEY 已加载:', !!KEY, KEY ? '(len=' + KEY.length + ')' : '')
|
||
console.log('[env] MaaS host:', MaaS ? MaaS.replace(/\/compatible-mode.*$/, '/…') : '(空)')
|
||
console.log('[out]', OUT)
|
||
|
||
const get = async (url, headers) => {
|
||
const r = await fetch(url, { headers, signal: AbortSignal.timeout(25000) })
|
||
const t = await r.text()
|
||
return { status: r.status, body: t }
|
||
}
|
||
|
||
/* ---------- 1. 先看这个 MaaS 端点声明了哪些模型 ---------- */
|
||
async function listModels() {
|
||
console.log('\n======== 1. 列出 MaaS 端点的模型 ========')
|
||
if (!MaaS) { console.log('无 MaaS host,跳过'); return [] }
|
||
try {
|
||
const r = await get(MaaS + '/models', { Authorization: 'Bearer ' + KEY })
|
||
console.log('status=', r.status)
|
||
if (r.status !== 200) { console.log(r.body.slice(0, 400)); return [] }
|
||
let j = null
|
||
try { j = JSON.parse(r.body) } catch { console.log(r.body.slice(0, 400)); return [] }
|
||
const ids = (j.data || []).map((m) => m.id)
|
||
console.log('模型数 =', ids.length)
|
||
const likely = ids.filter((id) => /image|t2i|wanx|wan2|flux|sd|diffus/i.test(id))
|
||
console.log('疑似文生图相关:', likely.length ? likely.join(', ') : '(无)')
|
||
console.log('全部模型(前 60):\n ' + ids.slice(0, 60).join('\n '))
|
||
return ids
|
||
} catch (e) { console.log('ERR', e.message) ; return [] }
|
||
}
|
||
|
||
/* ---------- 2. 探 text2image/image-synthesis 端点 ---------- */
|
||
const WANX_MODELS = [
|
||
'wanx2.1-t2i-turbo',
|
||
'wanx2.1-t2i-plus',
|
||
'wanx2.0-t2i-turbo',
|
||
'wanx-v1',
|
||
'wan2.2-t2i-flash',
|
||
'wan2.2-t2i-plus',
|
||
'wan2.5-t2i-preview',
|
||
'wan2.7-t2i-plus'
|
||
]
|
||
|
||
async function tryImageSynthesis(model) {
|
||
const url = PUBLIC_BASE + '/api/v1/services/aigc/text2image/image-synthesis'
|
||
try {
|
||
const r = await fetch(url, {
|
||
method: 'POST',
|
||
headers: {
|
||
Authorization: 'Bearer ' + KEY,
|
||
'Content-Type': 'application/json',
|
||
'X-DashScope-Async': 'enable'
|
||
},
|
||
body: JSON.stringify({
|
||
model,
|
||
input: { prompt: promptA },
|
||
parameters: { size: '1024*576', n: 1 }
|
||
}),
|
||
signal: AbortSignal.timeout(25000)
|
||
})
|
||
const t = await r.text()
|
||
let j = null
|
||
try { j = JSON.parse(t) } catch {}
|
||
if (r.status === 200 && j && j.output && j.output.task_id) {
|
||
console.log(' ✓ ' + model + ' task=' + j.output.task_id)
|
||
return j.output.task_id
|
||
}
|
||
const msg = (j && (j.message || (j.output && j.output.task_status))) || t.slice(0, 160)
|
||
console.log(' ✗ ' + model + ' HTTP ' + r.status + ' ' + String(msg).slice(0, 160))
|
||
return null
|
||
} catch (e) {
|
||
console.log(' ✗ ' + model + ' ERR ' + e.message)
|
||
return null
|
||
}
|
||
}
|
||
|
||
async function poll(taskId, maxTry) {
|
||
const url = PUBLIC_BASE + '/api/v1/tasks/' + taskId
|
||
for (let i = 0; i < (maxTry || 20); i++) {
|
||
await new Promise((r) => setTimeout(r, 3000))
|
||
const r = await get(url, { Authorization: 'Bearer ' + KEY })
|
||
let j = null
|
||
try { j = JSON.parse(r.body) } catch {}
|
||
const st = j && j.output && j.output.task_status
|
||
if (st === 'SUCCEEDED') {
|
||
const results = (j.output.results || []).map((x) => x.url).filter(Boolean)
|
||
return { status: st, urls: results }
|
||
}
|
||
if (st === 'FAILED' || st === 'UNKNOWN') {
|
||
return { status: st, msg: j.message || (j.output && j.output.message) }
|
||
}
|
||
}
|
||
return { status: 'TIMEOUT' }
|
||
}
|
||
|
||
/* ---------- 3. 探多模态生成端点(qwen-image 走 messages) ---------- */
|
||
const QWEN_IMAGE_MODELS = ['qwen-image', 'qwen-image-plus', 'qwen-image-max']
|
||
|
||
async function tryMultimodal(model) {
|
||
const url = PUBLIC_BASE + '/api/v1/services/aigc/multimodal-generation/generation'
|
||
try {
|
||
const r = await fetch(url, {
|
||
method: 'POST',
|
||
headers: { Authorization: 'Bearer ' + KEY, 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({
|
||
model,
|
||
input: { messages: [{ role: 'user', content: [{ text: promptA }] }] },
|
||
parameters: { size: '1024*576', n: 1 }
|
||
}),
|
||
signal: AbortSignal.timeout(40000)
|
||
})
|
||
const t = await r.text()
|
||
let j = null
|
||
try { j = JSON.parse(t) } catch {}
|
||
const content = j && j.output && j.output.choices && j.output.choices[0] &&
|
||
j.output.choices[0].message && j.output.choices[0].message.content
|
||
const urls = Array.isArray(content) ? content.map((c) => c.image).filter(Boolean) : []
|
||
if (r.status === 200 && urls.length) { console.log(' ✓ ' + model + ' 直接返回图片'); return { status: 'SUCCEEDED', urls } }
|
||
if (r.status === 200 && j && j.output && j.output.task_id) { console.log(' ✓ ' + model + ' task=' + j.output.task_id); return { status: 'TASK', task: j.output.task_id } }
|
||
const msg = (j && j.message) || t.slice(0, 160)
|
||
console.log(' ✗ ' + model + ' HTTP ' + r.status + ' ' + String(msg).slice(0, 160))
|
||
return null
|
||
} catch (e) { console.log(' ✗ ' + model + ' ERR ' + e.message); return null }
|
||
}
|
||
|
||
/* ---------- 4. 下载两张图,比对 MD5 ---------- */
|
||
async function download(url, name) {
|
||
const r = await fetch(url, { signal: AbortSignal.timeout(60000) })
|
||
if (!r.ok) throw new Error('HTTP ' + r.status)
|
||
const buf = Buffer.from(await r.arrayBuffer())
|
||
const file = path.join(OUT, name)
|
||
fs.writeFileSync(file, buf)
|
||
return { file, size: buf.length, md5: crypto.createHash('md5').update(buf).digest('hex') }
|
||
}
|
||
|
||
async function main() {
|
||
const available = await listModels()
|
||
|
||
console.log('\n======== 2. text2image/image-synthesis 候选模型 ========')
|
||
const hit = []
|
||
for (const m of WANX_MODELS) {
|
||
const t = await tryImageSynthesis(m)
|
||
if (t) hit.push({ model: m, task: t, kind: 'synthesis' })
|
||
}
|
||
|
||
console.log('\n======== 3. multimodal-generation 候选模型 ========')
|
||
for (const m of QWEN_IMAGE_MODELS) {
|
||
if (available.length && available.indexOf(m) < 0) { console.log(' - ' + m + ' 不在 MaaS 模型列表,仍试一次'); }
|
||
const r = await tryMultimodal(m)
|
||
if (r && r.status === 'SUCCEEDED') hit.push({ model: m, urls: r.urls, kind: 'multimodal' })
|
||
else if (r && r.status === 'TASK') hit.push({ model: m, task: r.task, kind: 'multimodal' })
|
||
}
|
||
|
||
if (!hit.length) { console.log('\n结论:没有可用的文生图模型组合。'); return }
|
||
|
||
const first = hit[0]
|
||
console.log('\n======== 4. 用可用模型 ' + first.model + ' 跑两张不同 prompt,比对 MD5 ========')
|
||
let urls = first.urls
|
||
if (!urls) {
|
||
const p = await poll(first.task, 30)
|
||
console.log('poll:', p.status, p.msg || '')
|
||
urls = p.urls || []
|
||
}
|
||
if (!urls.length) { console.log('未拿到图片 URL,无法比对'); return }
|
||
console.log('URL 数 =', urls.length, urls[0].slice(0, 100))
|
||
|
||
const a1 = await download(urls[0], 'probe_A.jpg')
|
||
console.log('A:', a1.size, a1.md5)
|
||
|
||
// 换 prompt 再生成一张
|
||
let taskB = null
|
||
if (first.kind === 'synthesis') taskB = await tryImageSynthesisWith(first.model, promptB)
|
||
if (taskB) {
|
||
const p = await poll(taskB, 30)
|
||
if (p.urls && p.urls.length) {
|
||
const b1 = await download(p.urls[0], 'probe_B.jpg')
|
||
console.log('B:', b1.size, b1.md5)
|
||
console.log('\n两张图 MD5 是否相同:', a1.md5 === b1.md5 ? '❌ 相同(模型没区分 prompt)' : '✓ 不同(模型正常按 prompt 出图)')
|
||
} else console.log('B 图未生成:', p.status)
|
||
}
|
||
}
|
||
|
||
async function tryImageSynthesisWith(model, prompt) {
|
||
const url = PUBLIC_BASE + '/api/v1/services/aigc/text2image/image-synthesis'
|
||
try {
|
||
const r = await fetch(url, {
|
||
method: 'POST',
|
||
headers: { Authorization: 'Bearer ' + KEY, 'Content-Type': 'application/json', 'X-DashScope-Async': 'enable' },
|
||
body: JSON.stringify({ model, input: { prompt }, parameters: { size: '1024*576', n: 1 } }),
|
||
signal: AbortSignal.timeout(25000)
|
||
})
|
||
const j = JSON.parse(await r.text())
|
||
return j && j.output && j.output.task_id ? j.output.task_id : null
|
||
} catch { return null }
|
||
}
|
||
|
||
main().catch((e) => console.error('FATAL', e && e.message)) |