massive update to hints and add localization engine

Signed-off-by: Vladimir Mandic <mandic00@live.com>
This commit is contained in:
Vladimir Mandic
2025-02-08 15:15:59 -05:00
parent 8873b2f696
commit a19ec070cf
21 changed files with 64820 additions and 1626 deletions
+9 -1
View File
@@ -1,7 +1,15 @@
# Change Log for SD.Next
## Update for 2025-02-07
## Update for 2025-02-08
- **Hints**
- added/updated 100+ ui hints!
- [hints](https://github.com/vladmandic/wiki/Hints) documentation and contribution guide
- **Localization**
- full ui localization!
english, croatian, spanish, french, italian, portuguese, chinese, japanese, korean, russian
- set in *settings -> user interface -> language*
- [localization](https://github.com/vladmandic/wiki/Locale) documentation
- **Torch**:
- for **zluda** set default to `torch==2.6.0+cu126`
- **Other**:
+4
View File
@@ -1,5 +1,9 @@
# TODO
- Check wiki links
- Update hints wiki
- Update changelog with docs and hints
Main ToDo list can be found at [GitHub projects](https://github.com/users/vladmandic/projects)
## Pending
+66
View File
@@ -0,0 +1,66 @@
#!/usr/bin/env node
// script used to localize sdnext ui and hints to multiple languages using google gemini ai
const fs = require('fs');
const process = require('process');
const { GoogleGenerativeAI } = require('@google/generative-ai');
const api_key = process.env.GOOGLE_AI_API_KEY;
const model = 'gemini-2.0-flash-exp';
const prompt = `
Translate attached JSON from English to {language} using following rules: fields id and label should be preserved from original, field localized should be a translated version of field label and field hint should be translated in-place.
Every JSON entry should have id, label, localized 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();
+1 -1
View File
@@ -7,7 +7,7 @@ from rich import print # pylint: disable=redefined-builtin
if __name__ == "__main__":
sys.argv.pop(0)
fn = sys.argv[0] if len(sys.argv) > 0 else 'locale_en.json'
fn = sys.argv[0] if len(sys.argv) > 0 else 'html/locale_en.json'
if not os.path.isfile(fn):
print(f'File not found: {fn}')
sys.exit(1)
+6872
View File
File diff suppressed because it is too large Load Diff
+1024 -955
View File
File diff suppressed because it is too large Load Diff
+6872
View File
File diff suppressed because it is too large Load Diff
+6872
View File
File diff suppressed because it is too large Load Diff
+1782
View File
File diff suppressed because it is too large Load Diff
+6872
View File
File diff suppressed because it is too large Load Diff
+6872
View File
File diff suppressed because it is too large Load Diff
+6872 -643
View File
File diff suppressed because it is too large Load Diff
+6872
View File
File diff suppressed because it is too large Load Diff
+6872
View File
File diff suppressed because it is too large Load Diff
+6872
View File
File diff suppressed because it is too large Load Diff
+5 -4
View File
@@ -4,6 +4,7 @@ import sys
import json
import time
import shutil
import locale
import logging
import platform
import subprocess
@@ -366,9 +367,8 @@ def git(arg: str, folder: str = None, ignore: bool = False, optional: bool = Fal
t_start = time.time()
if args.skip_git:
return ''
if optional:
if 'google.colab' in sys.modules:
return ''
if 'google.colab' in sys.modules:
return ''
git_cmd = os.environ.get('GIT', "git")
if git_cmd != "git":
git_cmd = os.path.abspath(git_cmd)
@@ -481,6 +481,7 @@ def get_platform():
'system': platform.system(),
'release': release,
'python': platform.python_version(),
'locale': locale.getlocale(),
'docker': os.environ.get('SD_DOCKER', None) is not None,
# 'host': platform.node(),
# 'version': platform.version(),
@@ -1305,7 +1306,7 @@ def check_venv():
t_start = time.time()
import site
pkg_path = [try_relpath(p) for p in site.getsitepackages() if os.path.exists(p)]
log.debug(f'Packages: venv={try_relpath(sys.prefix)} site={pkg_path}')
log.debug(f'Packages: prefix={try_relpath(sys.prefix)} site={pkg_path}')
for p in pkg_path:
invalid = []
for f in os.listdir(p):
+1
View File
@@ -132,6 +132,7 @@ div#extras_scale_to_tab div.form { flex-direction: row; }
width: 22em; min-height: 1.3em; font-size: var(--text-xs); transition: opacity 0.2s ease-in; pointer-events: none; opacity: 0; z-index: 999; }
.tooltip-show { opacity: 0.9; }
.toolbutton-selected { background: var(--background-fill-primary) !important; }
.locale { position: fixed; top: 1em; right: 1em; background-color: none; padding: 0.1em; width: 1.2em; height: 1.2em; cursor: pointer; font-size: 0.8em; font-weight: 800; font-family: monospace; }
#txt2img_hdr_color_row > div { min-width: unset !important; max-width: unset !important; }
#txt2img_advanced_options, #img2img_advanced_options, #control_advanced_options { min-width: 100%; }
#txt2img_advanced_options .gradio-checkbox, #img2img_advanced_options .gradio-checkbox, #control_advanced_options .gradio-checkbox { min-width: unset !important; max-width: fit-content; }
+73 -21
View File
@@ -1,17 +1,40 @@
const allLocales = ['en', 'de', 'es', 'fr', 'it', 'ja', 'ko', 'pt', 'hr', 'ru', 'zh'];
const localeData = {
prev: null,
locale: null,
data: [],
timeout: null,
finished: false,
type: 2,
el: null,
hint: null,
btn: null,
};
async function cycleLocale() {
console.log('cycleLocale', localeData.prev, localeData.locale);
const index = allLocales.indexOf(localeData.prev);
localeData.locale = allLocales[(index + 1) % allLocales.length];
localeData.btn.innerText = localeData.locale;
localeData.btn.style.backgroundColor = localeData.locale !== 'en' ? 'var(--primary-500)' : '';
localeData.finished = false;
localeData.data = [];
localeData.prev = localeData.locale;
window.opts.ui_locale = localeData.locale;
await setHints(); // eslint-disable-line no-use-before-define
}
async function tooltipCreate() {
localeData.el = document.createElement('div');
localeData.el.className = 'tooltip';
localeData.el.id = 'tooltip-container';
localeData.el.innerText = 'this is a hint';
gradioApp().appendChild(localeData.el);
localeData.hint = document.createElement('div');
localeData.hint.className = 'tooltip';
localeData.hint.id = 'tooltip-container';
localeData.hint.innerText = 'this is a hint';
gradioApp().appendChild(localeData.hint);
localeData.btn = document.createElement('div');
localeData.btn.className = 'locale';
localeData.btn.id = 'locale-container';
localeData.btn.innerText = localeData.locale;
localeData.btn.onclick = cycleLocale;
gradioApp().appendChild(localeData.btn);
if (window.opts.tooltips === 'None') localeData.type = 0;
if (window.opts.tooltips === 'Browser default') localeData.type = 1;
if (window.opts.tooltips === 'UI tooltips') localeData.type = 2;
@@ -19,18 +42,18 @@ async function tooltipCreate() {
async function tooltipShow(e) {
if (e.target.dataset.hint) {
localeData.el.classList.add('tooltip-show');
localeData.el.innerHTML = `<b>${e.target.textContent}</b><br>${e.target.dataset.hint}`;
localeData.hint.classList.add('tooltip-show');
localeData.hint.innerHTML = `<b>${e.target.textContent}</b><br>${e.target.dataset.hint}`;
if (e.clientX > window.innerWidth / 2) {
localeData.el.classList.add('tooltip-left');
localeData.hint.classList.add('tooltip-left');
} else {
localeData.el.classList.remove('tooltip-left');
localeData.hint.classList.remove('tooltip-left');
}
}
}
async function tooltipHide(e) {
localeData.el.classList.remove('tooltip-show');
localeData.hint.classList.remove('tooltip-show');
}
async function validateHints(json, elements) {
@@ -90,29 +113,57 @@ async function replaceButtonText(el) {
}
}
async function getLocaleData(desiredLocale = null) {
if (desiredLocale) desiredLocale = desiredLocale.split(':')[0];
if (desiredLocale === 'Auto') {
try {
localeData.locale = navigator.languages && navigator.languages.length ? navigator.languages[0] : navigator.language;
localeData.locale = localeData.locale.split('-')[0];
localeData.prev = localeData.locale;
} catch (e) {
localeData.locale = 'en';
log('getLocale', e);
}
} else {
localeData.locale = desiredLocale || 'en';
localeData.prev = localeData.locale;
}
log('getLocale', desiredLocale, localeData.locale);
let res = await fetch(`/file=html/locale_${localeData.locale}.json`);
if (!res || !res.ok) {
localeData.locale = 'en';
res = await fetch(`/file=html/locale_${localeData.locale}.json`);
}
const json = await res.json();
return json;
}
async function setHints(analyze = false) {
let json = {};
if (localeData.finished) return;
if (localeData.data.length === 0) {
const res = await fetch('/file=html/locale_en.json');
json = await res.json();
localeData.data = Object.values(json).flat().filter((e) => e.hint.length > 0);
for (const e of localeData.data) e.label = e.label.toLowerCase().trim();
}
if (Object.keys(opts).length === 0) return;
const elements = [
...Array.from(gradioApp().querySelectorAll('button')),
...Array.from(gradioApp().querySelectorAll('label > span')),
...Array.from(gradioApp().querySelectorAll('.label-wrap > span')),
];
if (elements.length === 0) return;
if (Object.keys(opts).length === 0) return;
if (!localeData.el) tooltipCreate();
if (localeData.data.length === 0) {
json = await getLocaleData(window.opts.ui_locale);
localeData.data = Object.values(json).flat().filter((e) => e.hint.length > 0);
for (const e of localeData.data) e.label = e.label.toLowerCase().trim();
}
if (!localeData.hint) tooltipCreate();
let localized = 0;
let hints = 0;
localeData.finished = true;
const t0 = performance.now();
for (const el of elements) {
const found = localeData.data.find((l) => l.label === el.textContent.toLowerCase().trim());
let found;
if (el.dataset.original) found = localeData.data.find((l) => l.label === el.dataset.original.toLowerCase().trim());
else found = localeData.data.find((l) => l.label === el.textContent.toLowerCase().trim());
if (found?.localized?.length > 0) {
if (!el.dataset.original) el.dataset.original = el.textContent;
localized++;
el.textContent = found.localized;
}
@@ -131,7 +182,8 @@ async function setHints(analyze = false) {
}
}
const t1 = performance.now();
log('setHints', { type: localeData.type, elements: elements.length, localized, hints, data: localeData.data.length, time: t1 - t0 });
localeData.btn.style.backgroundColor = localeData.locale !== 'en' ? 'var(--primary-500)' : '';
log('setHints', { type: localeData.type, locale: localeData.locale, elements: elements.length, localized, hints, data: localeData.data.length, time: t1 - t0 });
// sortUIElements();
if (analyze) {
const [missingHints, orphanedHints] = await validateHints(json, elements);
+1
View File
@@ -793,6 +793,7 @@ options_templates.update(options_section(('ui', "User Interface"), {
"theme_type": OptionInfo("Standard", "Theme type", gr.Radio, {"choices": ["Modern", "Standard", "None"]}),
"theme_style": OptionInfo("Auto", "Theme mode", gr.Radio, {"choices": ["Auto", "Dark", "Light"]}),
"gradio_theme": OptionInfo("black-teal", "UI theme", gr.Dropdown, lambda: {"choices": theme.list_themes()}, refresh=theme.refresh_themes),
"ui_locale": OptionInfo("Auto", "UI locale", gr.Dropdown, lambda: {"choices": theme.list_locales()}),
"autolaunch": OptionInfo(False, "Autolaunch browser upon startup"),
"font_size": OptionInfo(14, "Font size", gr.Slider, {"minimum": 8, "maximum": 32, "step": 1, "visible": True}),
"aspect_ratios": OptionInfo("1:1, 4:3, 3:2, 16:9, 16:10, 21:9, 2:3, 3:4, 9:16, 10:16, 9:21", "Allowed aspect ratios"),
+5 -1
View File
@@ -36,6 +36,10 @@ def refresh_themes(no_update=False):
return res
def list_locales():
return ['Auto', 'en: English', 'hr: Croatian', 'de: German', 'es: Spanish', 'fr: French', 'it: Italian', 'pt: Portuguese', 'zh: Chinese', 'ja: Japanese', 'ko: Korean', 'ru: Russian']
def list_themes():
extensions = [e.name for e in modules.extensions.extensions if e.enabled]
if 'sd-webui-lobe-theme' in extensions and modules.shared.opts.gradio_theme == 'lobe':
@@ -99,8 +103,8 @@ def reload_gradio_theme():
modules.shared.opts.theme_type = 'Standard'
theme_name = 'black-teal'
modules.shared.opts.data['gradio_theme'] = theme_name
modules.shared.log.info(f'UI locale: name="{modules.shared.opts.ui_locale}"')
if theme_name.lower() in ['lobe', 'cozy-nest']:
modules.shared.log.info(f'UI theme extension: name="{theme_name}"')
+1
View File
@@ -28,6 +28,7 @@
"esbuild": "^0.18.15"
},
"dependencies": {
"@google/generative-ai": "^0.21.0",
"argparse": "^2.0.1",
"eslint": "^8.57.0",
"eslint-config-airbnb-base": "^15.0.0",