Switched to danbooru.csv and improved tag parsing (closes #11)

This commit does a few things:
- Replaces autocomplete.txt with danbooru.csv, which contains aliases
  and categories
- Categries have been added to the autocompletion, changing the color of
  the tag and adding `(category)` after the tag.
- Aliases have been added to the autcompletion, automatically
  redirecting to their corresponding tag.
This commit is contained in:
mudknight
2026-01-01 23:07:32 -08:00
parent 10cc4fc80a
commit 7bec9656a5
5 changed files with 140977 additions and 146225 deletions
-146179
View File
File diff suppressed because it is too large Load Diff
+140782
View File
File diff suppressed because it is too large Load Diff
+87 -19
View File
@@ -35,37 +35,105 @@ export async function loadTags() {
export async function loadAutocompleteTags() {
try {
const response = await fetch('autocomplete.txt');
const response = await fetch('danbooru.csv');
if (!response.ok) {
console.log('Autocomplete file not found');
console.log('Danbooru CSV not found');
return [];
}
const text = await response.text();
const lines = text.split('\n');
const tags = lines
.map(line => {
const parts = line.trim().split(',');
if (parts.length >= 2) {
return {
tag: parts[0].trim(),
count: parseInt(parts[1]) || 0
};
const tags = [];
for (const line of lines) {
if (!line.trim()) continue;
// Parse CSV line handling quotes
const parts = parseCsvLine(line);
if (parts.length < 3) continue;
const tag = parts[0].trim();
const category = parseInt(parts[1]) || 0;
const count = parseInt(parts[2]) || 0;
const aliasField = parts[3] || '';
// Skip category 2 (unused)
if (category === 2) continue;
// Add main tag
tags.push({
tag: tag,
category: category,
count: count,
isAlias: false
});
// Parse and add aliases
if (aliasField) {
const aliases = parseAliases(aliasField);
for (const alias of aliases) {
tags.push({
tag: alias,
category: category,
count: count,
isAlias: true,
aliasFor: tag
});
}
return null;
})
.filter(Boolean)
.sort((a, b) => b.count - a.count);
console.log(`Loaded ${tags.length} autocomplete tags`);
}
}
// Sort by count (descending)
tags.sort((a, b) => b.count - a.count);
console.log(`Loaded ${tags.length} tags from Danbooru CSV`);
return tags;
} catch (error) {
console.error('Error loading autocomplete tags:', error);
console.error('Error loading Danbooru tags:', error);
return [];
}
}
function parseCsvLine(line) {
const parts = [];
let current = '';
let inQuotes = false;
for (let i = 0; i < line.length; i++) {
const char = line[i];
if (char === '"') {
inQuotes = !inQuotes;
} else if (char === ',' && !inQuotes) {
parts.push(current);
current = '';
} else {
current += char;
}
}
if (current) {
parts.push(current);
}
return parts;
}
function parseAliases(aliasField) {
if (!aliasField) return [];
// Remove quotes and leading/trailing slashes
let cleaned = aliasField.replace(/^["\/]+|["\/]+$/g, '');
// Split by comma
const aliases = cleaned.split(',')
.map(a => a.trim())
.filter(a => a.length > 0);
return aliases;
}
export async function saveCharacters(characters) {
const response = await fetch('/character_editor', {
method: 'POST',
+68 -27
View File
@@ -47,27 +47,26 @@ function detectContext(input) {
function showAutocomplete(input, context) {
const { type, searchTerm, start } = context;
// For LoRA and embedding, show immediately after typing the prefix
// For LoRA and embedding, show immediately after typing prefix
// For tags, require at least 2 characters
if (type === 'tag' && (!searchTerm || searchTerm.length < 2)) {
hideAutocomplete();
return;
}
// For LoRA/embedding, allow showing with 0 characters
// (right after typing <lora: or embedding:)
if ((type === 'lora' || type === 'embedding') &&
searchTerm === undefined) {
searchTerm === undefined) {
hideAutocomplete();
return;
}
autocompleteState.contextType = type;
let filtered = [];
console.log(`Searching ${type}:`, searchTerm);
// Filter based on context type
if (type === 'lora') {
const searchLower = searchTerm.toLowerCase();
@@ -98,7 +97,7 @@ function showAutocomplete(input, context) {
}));
console.log('Filtered embeddings:', filtered.length);
} else {
// Tag search
// Tag search with category and alias support
const searchLower = searchTerm.toLowerCase();
filtered = autocompleteState.tags
.filter(item =>
@@ -107,49 +106,81 @@ function showAutocomplete(input, context) {
.slice(0, 10)
.map(item => ({
display: item.tag,
value: item.tag,
value: item.isAlias ? item.aliasFor : item.tag,
count: item.count,
category: item.category,
isAlias: item.isAlias,
aliasFor: item.aliasFor,
type: 'tag'
}));
}
if (filtered.length === 0) {
hideAutocomplete();
return;
}
autocompleteState.filteredTags = filtered;
autocompleteState.selectedIndex = 0;
autocompleteState.activeElement = input;
autocompleteState.currentWord = searchTerm;
autocompleteState.wordStart = start;
const dropdown = document.getElementById('autocompleteDropdown');
dropdown.innerHTML = '';
filtered.forEach((item, index) => {
const div = document.createElement('div');
div.className = 'autocomplete-item';
if (index === 0) {
div.classList.add('selected');
}
// Different display based on type
if (item.type === 'tag' && item.count) {
div.innerHTML = `
<span class="autocomplete-tag">${item.display}</span>
<span class="autocomplete-count">${item.count}</span>
`;
} else {
div.innerHTML = `
<span class="autocomplete-tag">${item.display}</span>
`;
// Add category class for tags
if (item.type === 'tag' && item.category !== undefined) {
div.classList.add(`tag-category-${item.category}`);
}
// Different display based on type
if (item.type === 'tag') {
if (item.isAlias) {
// Alias format: alias -> tag (category) count
const categoryLabel = getCategoryLabel(item.category);
div.innerHTML = `
<span class="autocomplete-tag">
<span class="alias-name">${item.display}</span>
<span class="alias-arrow"> → </span>
<span class="alias-target">${item.value}</span>
${categoryLabel ?
`<span class="category-label">${categoryLabel}
</span>` : ''}
</span>
<span class="autocomplete-count">${item.count}</span>
`;
} else {
// Regular tag format: tag (category) count
const categoryLabel = getCategoryLabel(item.category);
div.innerHTML = `
<span class="autocomplete-tag">
${item.display}
${categoryLabel ?
`<span class="category-label">${categoryLabel}
</span>` : ''}
</span>
<span class="autocomplete-count">${item.count}</span>
`;
}
} else {
// LoRA/embedding format
div.innerHTML = `
<span class="autocomplete-tag">${item.display}</span>
`;
}
div.onclick = () => selectAutocomplete(index);
dropdown.appendChild(div);
});
const rect = input.getBoundingClientRect();
dropdown.style.left = rect.left + 'px';
dropdown.style.top = (rect.bottom + 5) + 'px';
@@ -157,6 +188,16 @@ function showAutocomplete(input, context) {
dropdown.style.display = 'block';
}
function getCategoryLabel(category) {
const labels = {
1: '(artist)',
3: '(copyright)',
4: '(character)',
5: '(meta)'
};
return labels[category] || '';
}
export function hideAutocomplete() {
const dropdown = document.getElementById('autocompleteDropdown');
dropdown.style.display = 'none';
+40
View File
@@ -683,6 +683,9 @@ textarea {
.autocomplete-tag {
flex: 1;
display: flex;
align-items: center;
gap: 4px;
}
.autocomplete-count {
@@ -691,6 +694,43 @@ textarea {
margin-left: 10px;
}
/* Category-specific colors */
.tag-category-1 .autocomplete-tag {
color: #ff6b6b;
}
.tag-category-3 .autocomplete-tag {
color: #b794f4;
}
.tag-category-4 .autocomplete-tag {
color: #68d391;
}
.tag-category-5 .autocomplete-tag {
color: #f6e05e;
}
/* Alias display styles */
.alias-name {
color: #888;
font-style: italic;
}
.alias-arrow {
color: #666;
}
.alias-target {
font-weight: 600;
}
.category-label {
font-size: 11px;
color: #888;
margin-left: 4px;
}
.autocomplete-dropdown::-webkit-scrollbar {
width: 8px;
}