mirror of https://github.com/vladmandic/automatic
parent
6876d2b84d
commit
73b90c5228
15
CHANGELOG.md
15
CHANGELOG.md
|
|
@ -1,7 +1,15 @@
|
||||||
# Change Log for SD.Next
|
# Change Log for SD.Next
|
||||||
|
|
||||||
## Update for 2026-02-12
|
## Update for 2026-02-14
|
||||||
|
|
||||||
|
### Highlights for 2026-02-14
|
||||||
|
|
||||||
|
TBD
|
||||||
|
|
||||||
|
### Details for 2026-02-14
|
||||||
|
|
||||||
|
- **Models**
|
||||||
|
- TBD
|
||||||
- **Image manipulation**
|
- **Image manipulation**
|
||||||
- use high-quality [sharpfin](https://github.com/drhead/Sharpfin) accelerated library
|
- use high-quality [sharpfin](https://github.com/drhead/Sharpfin) accelerated library
|
||||||
when available (cuda-only), thanks @CalamitousFelicitousness
|
when available (cuda-only), thanks @CalamitousFelicitousness
|
||||||
|
|
@ -18,6 +26,11 @@
|
||||||
instead of being used implicitly via quantization, thanks @CalamitousFelicitousness
|
instead of being used implicitly via quantization, thanks @CalamitousFelicitousness
|
||||||
- removed: old `codeformer` and `gfpgan` face restorers, thanks @CalamitousFelicitousness
|
- removed: old `codeformer` and `gfpgan` face restorers, thanks @CalamitousFelicitousness
|
||||||
- **UI**
|
- **UI**
|
||||||
|
- ui: **localization** improved translation quality and new translations locales:
|
||||||
|
*en, en1, en2, en3, en4, hr, es, it, fr, de, pt, ru, zh, ja, ko, hi, ar, bn, ur, id, vi, tr, sr, po, he, xx, yy, qq, tlh*
|
||||||
|
yes, this now includes stuff like *latin, esperanto, arabic, hebrew, klingon* and a lot more!
|
||||||
|
and also intruce some pseudo-locales such as: *techno-babbel*, *for-n00bs*
|
||||||
|
*hint*: click on locale icon in bottom-left corner to cycle through available locales, or set default in *settings -> ui*
|
||||||
- ui: **themes** add *CTD-NT64Light*, *CTD-NT64Medium* and *CTD-NT64Dark*, thanks @resonantsky
|
- ui: **themes** add *CTD-NT64Light*, *CTD-NT64Medium* and *CTD-NT64Dark*, thanks @resonantsky
|
||||||
- ui: **gallery** add option to auto-refresh gallery, thanks @awsr
|
- ui: **gallery** add option to auto-refresh gallery, thanks @awsr
|
||||||
- **Internal**
|
- **Internal**
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,7 @@ const jsConfig = defineConfig([
|
||||||
camelcase: 'off',
|
camelcase: 'off',
|
||||||
'default-case': 'off',
|
'default-case': 'off',
|
||||||
'max-classes-per-file': 'warn',
|
'max-classes-per-file': 'warn',
|
||||||
|
'guard-for-in': 'off',
|
||||||
'no-await-in-loop': 'off',
|
'no-await-in-loop': 'off',
|
||||||
'no-bitwise': 'off',
|
'no-bitwise': 'off',
|
||||||
'no-continue': 'off',
|
'no-continue': 'off',
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit ddf821483c8bcdac4868f15a9a838b45b4dd30ad
|
Subproject commit 264ac9f38a718b37ca2c6ce17fd906d5d153cb2e
|
||||||
|
|
@ -1 +1 @@
|
||||||
Subproject commit d25f3ee3a012d8a99cd6ded09152126a877b742e
|
Subproject commit fc7cf10dcc3f17377b6a18c4cd0dbd2be5480f0b
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
19349
html/locale_de.json
19349
html/locale_de.json
File diff suppressed because it is too large
Load Diff
3033
html/locale_en.json
3033
html/locale_en.json
File diff suppressed because it is too large
Load Diff
17427
html/locale_es.json
17427
html/locale_es.json
File diff suppressed because it is too large
Load Diff
16805
html/locale_fr.json
16805
html/locale_fr.json
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
17175
html/locale_hr.json
17175
html/locale_hr.json
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
16905
html/locale_it.json
16905
html/locale_it.json
File diff suppressed because it is too large
Load Diff
16909
html/locale_ja.json
16909
html/locale_ja.json
File diff suppressed because it is too large
Load Diff
18017
html/locale_ko.json
18017
html/locale_ko.json
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
16435
html/locale_pt.json
16435
html/locale_pt.json
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
15361
html/locale_ru.json
15361
html/locale_ru.json
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
18821
html/locale_zh.json
18821
html/locale_zh.json
File diff suppressed because it is too large
Load Diff
14
installer.py
14
installer.py
|
|
@ -688,23 +688,23 @@ def check_diffusers():
|
||||||
t_start = time.time()
|
t_start = time.time()
|
||||||
if args.skip_all:
|
if args.skip_all:
|
||||||
return
|
return
|
||||||
sha = '5bf248ddd8796b4f4958559429071a28f9b2dd3a' # diffusers commit hash
|
target_commit = '6141ae2348c1af14836ac076bf089a0259daa340' # diffusers commit hash
|
||||||
# if args.use_rocm or args.use_zluda or args.use_directml:
|
# if args.use_rocm or args.use_zluda or args.use_directml:
|
||||||
# sha = '043ab2520f6a19fce78e6e060a68dbc947edb9f9' # lock diffusers versions for now
|
# sha = '043ab2520f6a19fce78e6e060a68dbc947edb9f9' # lock diffusers versions for now
|
||||||
pkg = pkg_resources.working_set.by_key.get('diffusers', None)
|
pkg = pkg_resources.working_set.by_key.get('diffusers', None)
|
||||||
minor = int(pkg.version.split('.')[1] if pkg is not None else -1)
|
minor = int(pkg.version.split('.')[1] if pkg is not None else -1)
|
||||||
cur = opts.get('diffusers_version', '') if minor > -1 else ''
|
current = opts.get('diffusers_version', '') if minor > -1 else ''
|
||||||
if (minor == -1) or ((cur != sha) and (not args.experimental)):
|
if (minor == -1) or ((current != target_commit) and (not args.experimental)):
|
||||||
if minor == -1:
|
if minor == -1:
|
||||||
log.info(f'Diffusers install: commit={sha}')
|
log.info(f'Diffusers install: commit={target_commit}')
|
||||||
else:
|
else:
|
||||||
log.info(f'Diffusers update: current={pkg.version} hash={cur} target={sha}')
|
log.info(f'Diffusers update: current={pkg.version} hash={current} target={target_commit}')
|
||||||
pip('uninstall --yes diffusers', ignore=True, quiet=True, uv=False)
|
pip('uninstall --yes diffusers', ignore=True, quiet=True, uv=False)
|
||||||
if args.skip_git:
|
if args.skip_git:
|
||||||
log.warning('Git: marked as not available but required for diffusers installation')
|
log.warning('Git: marked as not available but required for diffusers installation')
|
||||||
pip(f'install --upgrade git+https://github.com/huggingface/diffusers@{sha}', ignore=False, quiet=True, uv=False)
|
pip(f'install --upgrade git+https://github.com/huggingface/diffusers@{target_commit}', ignore=False, quiet=True, uv=False)
|
||||||
global diffusers_commit # pylint: disable=global-statement
|
global diffusers_commit # pylint: disable=global-statement
|
||||||
diffusers_commit = sha
|
diffusers_commit = target_commit
|
||||||
ts('diffusers', t_start)
|
ts('diffusers', t_start)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
const allLocales = ['en', 'de', 'es', 'fr', 'it', 'ja', 'ko', 'pt', 'hr', 'ru', 'zh'];
|
const allLocales = ['en', 'tb', 'nb', 'hr', 'es', 'it', 'fr', 'de', 'pt', 'ru', 'zh', 'ja', 'ko', 'hi', 'ar', 'bn', 'ur', 'id', 'vi', 'tr', 'sr', 'po', 'he', 'xx', 'qq', 'tlh'];
|
||||||
const localeData = {
|
const localeData = {
|
||||||
prev: null,
|
prev: null,
|
||||||
locale: null,
|
locale: null,
|
||||||
|
|
@ -168,48 +168,6 @@ async function tooltipHide(e) {
|
||||||
localeData.currentElement = null;
|
localeData.currentElement = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function validateHints(json, elements, tab) {
|
|
||||||
json.missing = [];
|
|
||||||
const data = Object.values(json).flat().filter((e) => e.hint.length > 0);
|
|
||||||
for (const e of data) e.label = e.label.trim();
|
|
||||||
if (tab) {
|
|
||||||
elements = elements.filter((el) => el.closest(`#${tab}_tabitem`)); // include only elements within specified tab
|
|
||||||
elements = elements.filter((el) => !el.closest(`#${tab}_scripts_tabitem`));
|
|
||||||
}
|
|
||||||
let original = elements.map((e) => e.textContent.toLowerCase().trim()).sort(); // should be case sensitive
|
|
||||||
let duplicateUI = original.filter((e, i, a) => a.indexOf(e.toLowerCase()) !== i).sort();
|
|
||||||
original = [...new Set(original)]; // remove duplicates
|
|
||||||
duplicateUI = [...new Set(duplicateUI)]; // remove duplicates
|
|
||||||
const current = data.map((e) => e.label.toLowerCase().trim()).sort(); // should be case sensitive
|
|
||||||
// log('all elements:', original);
|
|
||||||
// log('all hints:', current);
|
|
||||||
log('hints-differences', { elements: original.length, hints: current.length });
|
|
||||||
const missingHints = original.filter((e) => !current.includes(e.toLowerCase())).sort();
|
|
||||||
const orphanedHints = current.filter((e) => !original.includes(e.toLowerCase())).sort();
|
|
||||||
const duplicateHints = current.filter((e, i, a) => a.indexOf(e.toLowerCase()) !== i).sort();
|
|
||||||
log('duplicate hints:', duplicateHints);
|
|
||||||
log('duplicate labels:', duplicateUI);
|
|
||||||
return [missingHints, orphanedHints];
|
|
||||||
}
|
|
||||||
|
|
||||||
async function addMissingHints(json, missingHints) {
|
|
||||||
if (missingHints.length === 0) return;
|
|
||||||
json.missing = [];
|
|
||||||
for (const h of missingHints.sort()) {
|
|
||||||
if (h.length <= 1) continue;
|
|
||||||
json.missing.push({ id: '', label: h, localized: '', hint: h, longHint: '' }); // Add longHint property
|
|
||||||
}
|
|
||||||
log('missing hints', missingHints);
|
|
||||||
log('added missing hints:', { missing: json.missing });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function removeOrphanedHints(json, orphanedHints) {
|
|
||||||
const data = Object.values(json).flat().filter((e) => e.hint.length > 0);
|
|
||||||
for (const e of data) e.label = e.label.trim();
|
|
||||||
const orphaned = data.filter((e) => orphanedHints.includes(e.label.toLowerCase()));
|
|
||||||
log('orphaned hints:', { orphaned });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function replaceButtonText(el) {
|
async function replaceButtonText(el) {
|
||||||
// https://www.nerdfonts.com/cheat-sheet
|
// https://www.nerdfonts.com/cheat-sheet
|
||||||
// use unicode of icon with format nf-md-<icon>_circle
|
// use unicode of icon with format nf-md-<icon>_circle
|
||||||
|
|
@ -289,7 +247,58 @@ async function setHint(el, entry) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setHints(analyze = false) {
|
function createLocaleJSON() {
|
||||||
|
const excludeText = ['▼']; // add any common non-label elements to exclude
|
||||||
|
const ecxcludeIds = ['logo_nav']; // add any specific element IDs to exclude
|
||||||
|
const elements = [
|
||||||
|
...Array.from(gradioApp().querySelectorAll('button')),
|
||||||
|
...Array.from(gradioApp().querySelectorAll('h1')),
|
||||||
|
...Array.from(gradioApp().querySelectorAll('h2')),
|
||||||
|
...Array.from(gradioApp().querySelectorAll('h3')),
|
||||||
|
...Array.from(gradioApp().querySelectorAll('label > span')),
|
||||||
|
...Array.from(gradioApp().querySelectorAll('.label-wrap > span')),
|
||||||
|
];
|
||||||
|
const json = {};
|
||||||
|
const allSeen = {};
|
||||||
|
for (const el of elements) {
|
||||||
|
const label = el.textContent.trim();
|
||||||
|
if (!label || label.length < 1 || label.length > 1024) continue; // likely not UI element
|
||||||
|
if (excludeText.includes(label)) continue; // skip common non-label elements
|
||||||
|
if (ecxcludeIds.includes(el.id)) continue; // skip specific element IDs
|
||||||
|
|
||||||
|
let hint = el.dataset.hint || '';
|
||||||
|
if (hint.toLowerCase() === label.toLowerCase()) hint = ''; // skip if hint is same as label
|
||||||
|
if (Object.keys(allSeen).includes(label.toLowerCase())) {
|
||||||
|
if (hint.length === 0 || allSeen[label.toLowerCase()] === hint) continue; // seen this label and hint is empty or same as before
|
||||||
|
hint = allSeen[label.toLowerCase()]; // use existing hint for this label
|
||||||
|
}
|
||||||
|
allSeen[label.toLowerCase()] = hint; // track seen labels
|
||||||
|
|
||||||
|
let section = label[0].toLowerCase();
|
||||||
|
if (section >= '0' && section <= '9') section = '0';
|
||||||
|
if ((section < 'a' || section > 'z') && section !== '0') section = '_';
|
||||||
|
|
||||||
|
let ui = el.closest('.group-extension')
|
||||||
|
|| el.closest('.group-scripts')
|
||||||
|
|| el.closest('.main-tab')
|
||||||
|
|| el.closest('.settings_section')
|
||||||
|
|| el.closest('.tabitem')
|
||||||
|
|| 'other';
|
||||||
|
ui = ui?.id?.replace('_tabitem_parent', '').replace('_section_row', '');
|
||||||
|
if (ui?.includes('_script')) ui = ui?.split('_').slice(1).join('_').split(':')[0];
|
||||||
|
|
||||||
|
if (!json[section]) json[section] = [];
|
||||||
|
const entry = { id: el.id || '', label, localized: '', hint, ui };
|
||||||
|
json[section].push(entry); // add new entry
|
||||||
|
}
|
||||||
|
const sorted = Object.keys(json).sort().reduce((obj, key) => {
|
||||||
|
obj[key] = json[key];
|
||||||
|
return obj;
|
||||||
|
}, {});
|
||||||
|
console.log('localeJSON', sorted);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setHints() {
|
||||||
let json = {};
|
let json = {};
|
||||||
let overrideData = [];
|
let overrideData = [];
|
||||||
if (localeData.finished) return;
|
if (localeData.finished) return;
|
||||||
|
|
@ -311,6 +320,7 @@ async function setHints(analyze = false) {
|
||||||
let localized = 0;
|
let localized = 0;
|
||||||
let hints = 0;
|
let hints = 0;
|
||||||
const t0 = performance.now();
|
const t0 = performance.now();
|
||||||
|
|
||||||
for (const el of elements) {
|
for (const el of elements) {
|
||||||
// localize elements text
|
// localize elements text
|
||||||
let found;
|
let found;
|
||||||
|
|
@ -318,8 +328,8 @@ async function setHints(analyze = false) {
|
||||||
else found = localeData.data.find((l) => l.label.toLowerCase().trim() === el.textContent.toLowerCase().trim());
|
else found = localeData.data.find((l) => l.label.toLowerCase().trim() === el.textContent.toLowerCase().trim());
|
||||||
if (found?.localized?.length > 0) {
|
if (found?.localized?.length > 0) {
|
||||||
if (!el.dataset.original) el.dataset.original = el.textContent;
|
if (!el.dataset.original) el.dataset.original = el.textContent;
|
||||||
localized++;
|
|
||||||
replaceTextContent(el, found.localized);
|
replaceTextContent(el, found.localized);
|
||||||
|
localized++;
|
||||||
} else if (found?.label && !localeData.initial && (localeData.locale === 'en')) { // reset to english
|
} else if (found?.label && !localeData.initial && (localeData.locale === 'en')) { // reset to english
|
||||||
replaceTextContent(el, found.label);
|
replaceTextContent(el, found.label);
|
||||||
}
|
}
|
||||||
|
|
@ -336,20 +346,8 @@ async function setHints(analyze = false) {
|
||||||
log('touchDevice', isTouchDevice);
|
log('touchDevice', isTouchDevice);
|
||||||
log('setHints', { type: localeData.type, locale: localeData.locale, elements: elements.length, localized, hints, data: localeData.data.length, override: overrideData.length, time: Math.round(t1 - t0) });
|
log('setHints', { type: localeData.type, locale: localeData.locale, elements: elements.length, localized, hints, data: localeData.data.length, override: overrideData.length, time: Math.round(t1 - t0) });
|
||||||
// sortUIElements();
|
// sortUIElements();
|
||||||
if (analyze) {
|
|
||||||
log('analyzing hints', 'control_tabitem');
|
|
||||||
const [missingHints, orphanedHints] = await validateHints(json, elements);
|
|
||||||
await addMissingHints(json, missingHints);
|
|
||||||
await removeOrphanedHints(json, orphanedHints);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const analyzeHints = async () => {
|
|
||||||
localeData.finished = false;
|
|
||||||
localeData.data = [];
|
|
||||||
await setHints(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Apply hints to a single element immediately
|
// Apply hints to a single element immediately
|
||||||
async function applyHintToElement(el) {
|
async function applyHintToElement(el) {
|
||||||
if (!localeData.data || localeData.data.length === 0) return;
|
if (!localeData.data || localeData.data.length === 0) return;
|
||||||
|
|
|
||||||
|
|
@ -255,10 +255,9 @@ def check_quant(module: str = ''):
|
||||||
|
|
||||||
def check_nunchaku(module: str = ''):
|
def check_nunchaku(module: str = ''):
|
||||||
from modules import shared
|
from modules import shared
|
||||||
model_name = getattr(shared.opts, 'sd_model_checkpoint', '')
|
if 'nunchaku' not in shared.opts.sd_model_checkpoint.lower():
|
||||||
if '+nunchaku' not in model_name:
|
|
||||||
return False
|
return False
|
||||||
base_path = model_name.split('+')[0]
|
base_path = shared.opts.sd_model_checkpoint.split('+')[0]
|
||||||
for v in shared.reference_models.values():
|
for v in shared.reference_models.values():
|
||||||
if v.get('path', '') != base_path:
|
if v.get('path', '') != base_path:
|
||||||
continue
|
continue
|
||||||
|
|
|
||||||
|
|
@ -44,10 +44,10 @@
|
||||||
"eslint": "^9.39.2",
|
"eslint": "^9.39.2",
|
||||||
"eslint-config-airbnb-extended": "^3.0.0",
|
"eslint-config-airbnb-extended": "^3.0.0",
|
||||||
"eslint-plugin-promise": "^7.2.1",
|
"eslint-plugin-promise": "^7.2.1",
|
||||||
|
"@google/genai": "^1.41.0",
|
||||||
"globals": "^17.0.0"
|
"globals": "^17.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@google/generative-ai": "^0.24.1",
|
|
||||||
"argparse": "^2.0.1"
|
"argparse": "^2.0.1"
|
||||||
},
|
},
|
||||||
"//": {
|
"//": {
|
||||||
|
|
|
||||||
|
|
@ -1,67 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
// script used to localize sdnext ui and hints to multiple languages using google gemini ai
|
|
||||||
|
|
||||||
const fs = require('node:fs');
|
|
||||||
|
|
||||||
const { GoogleGenerativeAI } = require('@google/generative-ai');
|
|
||||||
|
|
||||||
const api_key = process.env.GOOGLE_AI_API_KEY;
|
|
||||||
const model = 'gemini-2.5-flash';
|
|
||||||
const prompt = `Translate attached JSON from English to {language} using following rules: fields id, label and reload should be preserved from original, field localized should be a translated version of field label and field hint should be translated in-place.
|
|
||||||
if field is less than 3 characters, do not translate it and keep it as is.
|
|
||||||
Every JSON entry should have id, label, localized, reload and hint fields.
|
|
||||||
Output should be pure JSON without any additional text. To better match translation, context of the text is related to Stable Diffusion and topic of Generative AI.`;
|
|
||||||
const languages = {
|
|
||||||
hr: 'Croatian',
|
|
||||||
de: 'German',
|
|
||||||
es: 'Spanish',
|
|
||||||
fr: 'French',
|
|
||||||
it: 'Italian',
|
|
||||||
pt: 'Portuguese',
|
|
||||||
zh: 'Chinese',
|
|
||||||
ja: 'Japanese',
|
|
||||||
ko: 'Korean',
|
|
||||||
ru: 'Russian',
|
|
||||||
};
|
|
||||||
const chunkLines = 100;
|
|
||||||
|
|
||||||
async function localize() {
|
|
||||||
if (!api_key || api_key.length < 10) {
|
|
||||||
console.error('localize: set GOOGLE_AI_API_KEY env variable with your API key');
|
|
||||||
process.exit();
|
|
||||||
}
|
|
||||||
const genAI = new GoogleGenerativeAI(api_key);
|
|
||||||
const instance = genAI.getGenerativeModel({ model });
|
|
||||||
const raw = fs.readFileSync('html/locale_en.json');
|
|
||||||
const json = JSON.parse(raw);
|
|
||||||
for (const locale of Object.keys(languages)) {
|
|
||||||
const lang = languages[locale];
|
|
||||||
const target = prompt.replace('{language}', lang).trim();
|
|
||||||
const output = {};
|
|
||||||
const fn = `html/locale_${locale}.json`;
|
|
||||||
for (const section of Object.keys(json)) {
|
|
||||||
const data = json[section];
|
|
||||||
output[section] = [];
|
|
||||||
for (let i = 0; i < data.length; i += chunkLines) {
|
|
||||||
let markdown;
|
|
||||||
try {
|
|
||||||
const chunk = data.slice(i, i + chunkLines);
|
|
||||||
const result = await instance.generateContent([target, JSON.stringify(chunk)]);
|
|
||||||
markdown = result.response.text();
|
|
||||||
const text = markdown.replaceAll('```', '').replace(/^.*\n/, '');
|
|
||||||
const parsed = JSON.parse(text);
|
|
||||||
output[section].push(...parsed);
|
|
||||||
console.log(`localize: locale=${locale} lang=${lang} section=${section} chunk=${chunk.length} output=${output[section].length} fn=${fn}`);
|
|
||||||
} catch (err) {
|
|
||||||
console.error('localize:', err);
|
|
||||||
console.error('localize input:', { target, section, i });
|
|
||||||
console.error('localize output:', { markdown });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const txt = JSON.stringify(output, null, 2);
|
|
||||||
fs.writeFileSync(fn, txt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
localize();
|
|
||||||
|
|
@ -0,0 +1,150 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
// script used to localize sdnext ui and hints to multiple languages using google gemini ai
|
||||||
|
|
||||||
|
import { GoogleGenAI, Type, ThinkingLevel } from '@google/genai';
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
import * as process from 'node:process';
|
||||||
|
|
||||||
|
const apiKey = process.env.GOOGLE_API_KEY;
|
||||||
|
const model = 'gemini-3-flash-preview';
|
||||||
|
const prompt = `## You are expert translator AI.
|
||||||
|
Translate attached JSON from English to {language}.
|
||||||
|
## Translation Rules:
|
||||||
|
- Fields \`id\`, \`label\` and \`reload\` should be preserved from original.
|
||||||
|
- Field \`localized\` should be a translated version of field \`label\`.
|
||||||
|
- If field \`localized\` is less than 3 characters, do not translate it and keep it as is.
|
||||||
|
- Field \`hint\` should be translated in-place.
|
||||||
|
- Use alphabet and writing system of the target language.
|
||||||
|
- Use terminology that is commonly used in the target language for software interfaces, especially in the context of Stable Diffusion and Generative AI.
|
||||||
|
- Do not leave non-translated words in the output, except for technical terms that do not have a widely accepted translation in the target language. In such cases, provide a transliteration or a brief explanation in parentheses.
|
||||||
|
- Ensure that all lines are present in the output, and that the output is valid JSON matching the provided schema.
|
||||||
|
`;
|
||||||
|
|
||||||
|
const languages = {
|
||||||
|
tb: 'English: Techno-Babble',
|
||||||
|
nb: 'English: For-N00bs',
|
||||||
|
hr: 'Croatian',
|
||||||
|
es: 'Spanish',
|
||||||
|
it: 'Italian',
|
||||||
|
xx: 'Esperanto',
|
||||||
|
qq: 'Latin',
|
||||||
|
fr: 'French',
|
||||||
|
de: 'German',
|
||||||
|
pt: 'Portuguese',
|
||||||
|
ru: 'Russian',
|
||||||
|
zh: 'Chinese',
|
||||||
|
ja: 'Japanese',
|
||||||
|
ko: 'Korean',
|
||||||
|
hi: 'Hindi',
|
||||||
|
ar: 'Arabic',
|
||||||
|
bn: 'Bengali',
|
||||||
|
ur: 'Urdu',
|
||||||
|
id: 'Indonesian',
|
||||||
|
vi: 'Vietnamese',
|
||||||
|
tr: 'Turkish',
|
||||||
|
sr: 'Serbian',
|
||||||
|
po: 'Polish',
|
||||||
|
he: 'Hebrew',
|
||||||
|
tlh: 'Klingon',
|
||||||
|
};
|
||||||
|
|
||||||
|
const responseSchema = {
|
||||||
|
type: Type.ARRAY,
|
||||||
|
items: {
|
||||||
|
type: Type.OBJECT,
|
||||||
|
properties: {
|
||||||
|
id: { type: Type.INTEGER, description: 'id of the item' },
|
||||||
|
label: { type: Type.STRING, description: 'original label of the item' },
|
||||||
|
localized: { type: Type.STRING, description: 'translated label of the item' },
|
||||||
|
reload: { type: Type.STRING, description: 'n/a' },
|
||||||
|
hint: { type: Type.STRING, description: 'long hint for the item' },
|
||||||
|
},
|
||||||
|
required: ['id', 'label', 'localized', 'reload', 'hint'],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async function localize() {
|
||||||
|
if (!apiKey || apiKey.length < 10) {
|
||||||
|
console.error('localize: set GOOGLE_API_KEY env variable with your API key');
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
const httpOptions = { timeout: 60000 };
|
||||||
|
const ai = new GoogleGenAI({ apiKey, httpOptions });
|
||||||
|
const thinkingConfig = {
|
||||||
|
includeThoughts: false,
|
||||||
|
thinkingLevel: ThinkingLevel.LOW,
|
||||||
|
};
|
||||||
|
|
||||||
|
const params = {
|
||||||
|
model,
|
||||||
|
contents: {
|
||||||
|
parts: [
|
||||||
|
{ text: 'prompt' },
|
||||||
|
{ text: 'data' },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
responseMimeType: 'application/json',
|
||||||
|
thinkingConfig,
|
||||||
|
responseSchema,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
console.log('params:', params);
|
||||||
|
|
||||||
|
const raw = fs.readFileSync('html/locale_en.json');
|
||||||
|
console.log('raw:', { bytes: raw.length });
|
||||||
|
const json = JSON.parse(raw);
|
||||||
|
console.log('targets:', { lang: Object.keys(languages), count: Object.keys(languages).length });
|
||||||
|
|
||||||
|
for (const index in Object.keys(languages)) {
|
||||||
|
const locale = Object.keys(languages)[index];
|
||||||
|
const lang = languages[locale];
|
||||||
|
const langPrompt = prompt.replace('{language}', lang).trim();
|
||||||
|
const output = {};
|
||||||
|
const fn = `html/locale_${locale}.json`;
|
||||||
|
if (fs.existsSync(fn)) {
|
||||||
|
console.log('skip:', { index, locale, lang, fn });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
console.log('localize:', { index, locale, lang, fn });
|
||||||
|
const t0 = performance.now();
|
||||||
|
let allOk = true;
|
||||||
|
for (const section of Object.keys(json)) {
|
||||||
|
const keys = Object.keys(json[section]).length;
|
||||||
|
console.log(' start:', { locale, section, keys });
|
||||||
|
try {
|
||||||
|
const t1 = performance.now();
|
||||||
|
const sectionJSON = json[section];
|
||||||
|
const sectionParams = { ...params };
|
||||||
|
sectionParams.contents.parts[0].text = langPrompt;
|
||||||
|
sectionParams.contents.parts[1].text = `## JSON Data: \n${JSON.stringify(sectionJSON)}`;
|
||||||
|
const response = await ai.models.generateContent(sectionParams);
|
||||||
|
const responseJSON = JSON.parse(response.text);
|
||||||
|
const diff = Math.abs(keys - responseJSON.length);
|
||||||
|
if (diff > 1) {
|
||||||
|
console.error(' error:', { locale, section, input: keys, output: responseJSON.length });
|
||||||
|
allOk = false;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
output[section] = JSON.parse(response.text);
|
||||||
|
const t2 = performance.now();
|
||||||
|
const kps = Math.round(1000000 * keys / (t2 - t1)) / 1000;
|
||||||
|
console.log(' end:', { locale, section, time: Math.round(t2 - t1) / 1000, kps });
|
||||||
|
} catch (err) {
|
||||||
|
allOk = false;
|
||||||
|
console.error(' error:', err);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
// break; // for testing, remove this to process all sections
|
||||||
|
}
|
||||||
|
if (allOk) {
|
||||||
|
const txt = JSON.stringify(output, null, 2);
|
||||||
|
fs.writeFileSync(fn, txt);
|
||||||
|
}
|
||||||
|
const t3 = performance.now();
|
||||||
|
console.log(' time:', { locale, time: Math.round(t3 - t0) / 1000 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
localize();
|
||||||
2
wiki
2
wiki
|
|
@ -1 +1 @@
|
||||||
Subproject commit 64e9a355dc17eef9e237f7d3c9c4bde09e5aa6b2
|
Subproject commit 8afa5d9c71b06d3cd7b81e8f6099d0d88e021c05
|
||||||
Loading…
Reference in New Issue