ExportCivitAIMetadata

导出 civitai.com 的 safetensors 模型元数据 / Export .safetensor file's metadata from civitAI

  1. // ==UserScript==
  2. // @name ExportCivitAIMetadata
  3. // @namespace https://github.com/magicFeirl/ExportCivitAIMetadata.git
  4. // @description 导出 civitai.com 的 safetensors 模型元数据 / Export .safetensor file's metadata from civitAI
  5. // @author ctrn43062
  6. // @match https://civitai.com/*
  7. // @icon https://www.google.com/s2/favicons?sz=64&domain=civitai.com
  8. // @version 0.8
  9. // @note 0.8 feat: 获取链接方式从 href -> version id
  10. // @note 0.7 fix: 适配新样式
  11. // @note 0.6 feat: 添加 cmd args 导出
  12. // @note 0.5 refactor: 重构代码
  13. // @note 0.4 fix: 修改获取文件名逻辑
  14. // @note 0.3 fix: 适配新版UI @gustproof
  15. // @note 0.2 fix: 修复某些情况下复制按钮没有出现的bug
  16. // @note 0.1 init
  17. // @license MIT
  18. // ==/UserScript==
  19.  
  20.  
  21. function downloadFile(filename, content, type = 'text/plain') {
  22. const a = document.createElement('a')
  23.  
  24. if (typeof content === 'object') {
  25. content = JSON.stringify(content, (k, v) => {
  26. try {
  27. return JSON.parse(v)
  28. } catch {
  29. return v
  30. }
  31. }, 2)
  32. }
  33.  
  34. const url = URL.createObjectURL(new Blob([content], {
  35. type
  36. }))
  37.  
  38. a.href = url
  39. a.download = filename || 'untitle.txt'
  40. a.click()
  41.  
  42. setTimeout(() => {
  43. URL.revokeObjectURL(url)
  44. a.remove()
  45. }, 0);
  46. }
  47.  
  48. /**
  49. * Usage: SafetensorReader.readMetadataFromURL(url)
  50. */
  51. class SafetensorReader {
  52. static async readLengthFromURL(url) {
  53. const resp = await fetch(url, {
  54. headers: {
  55. range: "bytes=0-7"
  56. },
  57. cors: "cors"
  58. })
  59.  
  60. return new DataView(await resp.arrayBuffer()).getBigUint64(0, true)
  61. }
  62.  
  63. static async readMetadataFromURL(url) {
  64. const metaLength = await SafetensorReader.readLengthFromURL(url)
  65. const resp = await fetch(url, {
  66. headers: {
  67. range: `bytes=8-${metaLength + 7n}`
  68. }
  69. })
  70.  
  71. return await resp.json()
  72. }
  73. }
  74.  
  75. class civitAI {
  76. SELECTORS = {
  77. // 模型下载链接按钮(旧版)
  78. downloadBtn: '.mantine-cr35cs',
  79. // 获取模型 versionId(新版)
  80. versionId: '.mantine-Code-root.mantine-1fw9g1n:last-child',
  81. modelTitle: '.mantine-127eswf',
  82. modelOprationGroup: '.mantine-Group-root .mantine-1dutd10:last-child',
  83. // last button
  84. thumbUpBtn: `.mantine-Group-root .mantine-1dutd10:last-child > :last-child`
  85. }
  86.  
  87. CMD_ARGS = ["ss_output_name", "ss_learning_rate", "ss_text_encoder_lr", "ss_unet_lr", "ss_gradient_checkpointing", "ss_gradient_accumulation_steps", "ss_max_train_steps", "ss_lr_warmup_steps", "ss_lr_scheduler", "ss_network_module", "ss_network_dim", "ss_network_alpha", "ss_network_dropout", "ss_mixed_precision", "ss_full_fp16", "ss_v2", "ss_clip_skip", "ss_max_token_length", "ss_cache_latents", "ss_seed", "ss_lowram", "ss_noise_offset", "ss_multires_noise_iterations", "ss_multires_noise_discount", "ss_adaptive_noise_scale", "ss_zero_terminal_snr", "ss_training_comment", "ss_max_grad_norm", "ss_caption_dropout_rate", "ss_caption_dropout_every_n_epochs", "ss_caption_tag_dropout_rate", "ss_face_crop_aug_range", "ss_prior_loss_weight", "ss_min_snr_gamma", "ss_scale_weight_norms", "ss_ip_noise_gamma", "ss_debiased_estimation", "ss_noise_offset_random_strength", "ss_ip_noise_gamma_random_strength", "ss_loss_type", "ss_huber_schedule", "ss_huber_c"]
  88.  
  89. getModelDownloadURL() {
  90. // const dlBtn = document.querySelector(this.SELECTORS.downloadBtn)
  91. // if (!dlBtn || !dlBtn.href) {
  92. // throw Error('无法找到下载按钮 / Can\'t find the download button')
  93. // }
  94.  
  95. const versionId = document.querySelector(this.SELECTORS.versionId)
  96. if(!versionId || !versionId.textContent) {
  97. throw Error('无法找到模型 VersionID / Can\'t find the model\'s version id')
  98. }
  99.  
  100. return `https://civitai.com/api/download/models/${versionId.textContent}`
  101. }
  102.  
  103. getModelTitle() {
  104. const titleEl = document.querySelector(this.SELECTORS.modelTitle)
  105. // 需要规则化
  106. const title = titleEl ? titleEl.innerText : "untitle"
  107. return title
  108. }
  109.  
  110. getModelPageId() {
  111. // /models/123 -> models_123
  112. return location.pathname.replace(/^\//, '').replace(/\//, '_')
  113. }
  114.  
  115.  
  116. static hasExportBtnInstalled() {
  117. return document.querySelector('#EXPORT_BTN_CONTAINER')
  118. }
  119.  
  120. createExportBtn() {
  121. if (civitAI.hasExportBtnInstalled()) {
  122. return
  123. }
  124.  
  125. const thumbUpBtn = document.querySelector(this.SELECTORS.thumbUpBtn)
  126. const exportBtnContainer = thumbUpBtn.cloneNode(true)
  127. const exportBtn = exportBtnContainer.querySelector('button') || exportBtnContainer
  128. // 2024-5-5 CivitAI 再次进行了样式更新
  129.  
  130. const group = document.querySelector(this.SELECTORS.modelOprationGroup)
  131. Object.assign(exportBtn.style , { width: '43px', 'height': '36px' })
  132.  
  133. exportBtn.setAttribute('title', 'Export model\'s metadata')
  134. try {
  135. const svgContainer = exportBtn.querySelector('.mantine-Button-label')
  136. svgContainer.innerHTML = `<?xml version="1.0" ?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools --><svg width="43px" height="36px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M10 8V7C10 6.05719 10 5.58579 10.2929 5.29289C10.5858 5 11.0572 5 12 5H17C17.9428 5 18.4142 5 18.7071 5.29289C19 5.58579 19 6.05719 19 7V12C19 12.9428 19 13.4142 18.7071 13.7071C18.4142 14 17.9428 14 17 14H16M7 19H12C12.9428 19 13.4142 19 13.7071 18.7071C14 18.4142 14 17.9428 14 17V12C14 11.0572 14 10.5858 13.7071 10.2929C13.4142 10 12.9428 10 12 10H7C6.05719 10 5.58579 10 5.29289 10.2929C5 10.5858 5 11.0572 5 12V17C5 17.9428 5 18.4142 5.29289 18.7071C5.58579 19 6.05719 19 7 19Z" stroke="#fff" stroke-linecap="round" stroke-linejoin="round"/></svg>`
  137. } catch {
  138.  
  139. }
  140.  
  141. // 添加唯一 ID,避免重复插入 node
  142. exportBtnContainer.id = "EXPORT_BTN_CONTAINER"
  143.  
  144. exportBtnContainer.addEventListener('click', () => {
  145. exportBtn.setAttribute('disabled', true)
  146.  
  147. this.exportMetadata().finally(() => {
  148. exportBtn.removeAttribute('disabled')
  149. })
  150. })
  151.  
  152. group.appendChild(exportBtnContainer)
  153. }
  154.  
  155. async getMetadata(modelDlUrl) {
  156. const raw_metadata = await SafetensorReader.readMetadataFromURL(modelDlUrl)
  157. const metadata = raw_metadata.__metadata__ || raw_metadata
  158.  
  159. return metadata
  160. }
  161.  
  162. async exportMetadata() {
  163. const filename = `${this.getModelPageId()}_${this.getModelTitle()}`
  164.  
  165. const metadata = await this.getMetadata(this.getModelDownloadURL())
  166.  
  167. if (!metadata || !Object.keys(metadata).length) {
  168. // no metadata
  169. alert('This model has no metadata')
  170. return
  171. }
  172.  
  173. downloadFile(`${filename}.json`, metadata)
  174.  
  175. const cmd_args = this.CMD_ARGS.map(arg => [arg.replace('ss_', ''), metadata[arg]]).filter(([name, value]) => value != undefined).map(([name, value]) => `--${name} ${value} \\`).join('\n')
  176. if (cmd_args) {
  177. downloadFile(`${filename}_cmd_args.txt`, cmd_args)
  178. }
  179.  
  180. console.log(filename, metadata);
  181. }
  182. }
  183.  
  184. /**
  185. * Single Page Application 页面变化检测
  186. *
  187. * 通过监听 title 变化实现页面变化检测
  188. */
  189. class SPAMonitor {
  190. constructor() {
  191. this.event = new CustomEvent('page-change')
  192. this.eventCallbacks = new Set()
  193.  
  194. this.dispacher = window
  195. this._initPageChangeListener()
  196. }
  197.  
  198. _initPageChangeListener() {
  199. const ob = new MutationObserver((mutations) => {
  200. this.dispacher.dispatchEvent(this.event)
  201. })
  202.  
  203. // 实际只是监听 title 变化
  204. // 某些网页会动态改变 title,可能会有 bug
  205. ob.observe(document.querySelector('title'), {
  206. childList: true,
  207. subtree: true
  208. })
  209. }
  210.  
  211. triggerPageChangeEvent() {
  212. this.dispacher.dispatchEvent(this.event)
  213. }
  214.  
  215. addPageChangeEventListener(cb) {
  216. if (!cb || !typeof cb === 'function') {
  217. return;
  218. }
  219.  
  220. const cbString = cb.toString()
  221. if (!this.eventCallbacks.has(cbString)) {
  222. this.dispacher.addEventListener('page-change', cb)
  223. this.eventCallbacks.add(cbString)
  224. }
  225. }
  226.  
  227. waitForElement(selector, timeout = 10 * 1000, interval = 50) {
  228. let findTimes = 0
  229.  
  230. const findEl = (resolve, reject) => {
  231. if (findTimes * interval >= timeout) {
  232. return reject()
  233. }
  234.  
  235. const el = document.querySelector(selector)
  236. console.log(`finding ${selector}`);
  237.  
  238. setTimeout(() => {
  239. if (!el) {
  240. findTimes++
  241. findEl(resolve, reject)
  242. } else {
  243. resolve(el)
  244. }
  245. }, interval);
  246. }
  247.  
  248. return new Promise((resolve, reject) => {
  249. findEl(resolve, reject)
  250. })
  251. }
  252. }
  253.  
  254. function main() {
  255. // 1. 等待页面加载完成
  256. // 2. 添加 export button
  257. // 3. 添加事件回调
  258.  
  259. const spa = new SPAMonitor()
  260. const civitai = new civitAI()
  261.  
  262. spa.addPageChangeEventListener(() => {
  263. const isModelPage = /^https?:\/\/civitai.com\/models\/\d+/.test(location.href)
  264.  
  265. if (!isModelPage) {
  266. return
  267. }
  268.  
  269. spa.waitForElement(civitai.SELECTORS.modelOprationGroup).then(() => {
  270. civitai.createExportBtn()
  271. })
  272. })
  273.  
  274. spa.triggerPageChangeEvent()
  275. }
  276.  
  277. (function () {
  278. main()
  279. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址