@zodic/shared 0.0.221 → 0.0.223
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/app/services/ConceptService.ts +169 -117
- package/package.json +1 -1
|
@@ -3,10 +3,12 @@ import {
|
|
|
3
3
|
KVNamespaceListResult,
|
|
4
4
|
} from '@cloudflare/workers-types';
|
|
5
5
|
import { and, eq, sql } from 'drizzle-orm';
|
|
6
|
+
import { drizzle } from 'drizzle-orm/d1';
|
|
6
7
|
import { inject, injectable } from 'inversify';
|
|
7
8
|
import 'reflect-metadata';
|
|
8
9
|
import { v4 as uuidv4 } from 'uuid';
|
|
9
10
|
import { schema } from '../..';
|
|
11
|
+
import { conceptsData } from '../../db/schema';
|
|
10
12
|
import { Concept, ControlNetConfig, Languages } from '../../types';
|
|
11
13
|
import {
|
|
12
14
|
KVConcept,
|
|
@@ -16,8 +18,6 @@ import {
|
|
|
16
18
|
import { leonardoInitImages } from '../../utils/initImages';
|
|
17
19
|
import { buildConceptKVKey } from '../../utils/KVKeysBuilders';
|
|
18
20
|
import { AppContext } from '../base/AppContext';
|
|
19
|
-
import { drizzle } from 'drizzle-orm/d1';
|
|
20
|
-
import { conceptsData } from '../../db/schema';
|
|
21
21
|
|
|
22
22
|
@injectable()
|
|
23
23
|
export class ConceptService {
|
|
@@ -1030,11 +1030,16 @@ export class ConceptService {
|
|
|
1030
1030
|
};
|
|
1031
1031
|
}
|
|
1032
1032
|
|
|
1033
|
-
async regenerateDuplicateNamesForConcept(
|
|
1034
|
-
|
|
1035
|
-
|
|
1033
|
+
async regenerateDuplicateNamesForConcept(
|
|
1034
|
+
conceptSlug: Concept,
|
|
1035
|
+
maxEntries?: number
|
|
1036
|
+
) {
|
|
1037
|
+
console.log(
|
|
1038
|
+
`🔄 [START] Regenerating duplicate names for concept: ${conceptSlug}...`
|
|
1039
|
+
);
|
|
1040
|
+
|
|
1036
1041
|
const db = drizzle(this.context.env.DB);
|
|
1037
|
-
|
|
1042
|
+
|
|
1038
1043
|
// ✅ Step 1: Fetch duplicate entries filtered by conceptSlug
|
|
1039
1044
|
const duplicateEntries = await db
|
|
1040
1045
|
.select({
|
|
@@ -1057,131 +1062,178 @@ export class ConceptService {
|
|
|
1057
1062
|
)
|
|
1058
1063
|
.limit(maxEntries || 50) // ✅ Apply optional limit
|
|
1059
1064
|
.execute();
|
|
1060
|
-
|
|
1065
|
+
|
|
1061
1066
|
if (!duplicateEntries.length) {
|
|
1062
1067
|
console.log(`🎉 No duplicates found for ${conceptSlug}.`);
|
|
1063
1068
|
return { message: `No duplicates detected for ${conceptSlug}.` };
|
|
1064
1069
|
}
|
|
1065
|
-
|
|
1066
|
-
console.log(
|
|
1067
|
-
|
|
1070
|
+
|
|
1071
|
+
console.log(
|
|
1072
|
+
`🔍 Found ${duplicateEntries.length} duplicate entries to process.`
|
|
1073
|
+
);
|
|
1074
|
+
|
|
1075
|
+
// ✅ Group duplicates by name
|
|
1076
|
+
const duplicateGroups = new Map<string, typeof duplicateEntries>();
|
|
1077
|
+
for (const entry of duplicateEntries) {
|
|
1078
|
+
if (!duplicateGroups.has(entry.name)) {
|
|
1079
|
+
duplicateGroups.set(entry.name, []);
|
|
1080
|
+
}
|
|
1081
|
+
duplicateGroups.get(entry.name)!.push(entry);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1068
1084
|
let recentDuplicates = new Set<string>(); // ✅ Track newly generated names
|
|
1069
1085
|
let updatedEntries = 0;
|
|
1070
|
-
|
|
1071
|
-
for (const
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1086
|
+
|
|
1087
|
+
for (const [duplicateName, entries] of duplicateGroups.entries()) {
|
|
1088
|
+
console.log(
|
|
1089
|
+
`🔄 Processing duplicate group: "${duplicateName}" with ${entries.length} occurrences`
|
|
1090
|
+
);
|
|
1091
|
+
|
|
1092
|
+
// ✅ Leave the first entry unchanged and only process the extras
|
|
1093
|
+
const [keepEntry, ...entriesToProcess] = entries;
|
|
1094
|
+
console.log(`✅ Keeping original name for ID: ${keepEntry.id}`);
|
|
1095
|
+
|
|
1096
|
+
for (const entry of entriesToProcess) {
|
|
1097
|
+
const { id, description, combination } = entry;
|
|
1098
|
+
|
|
1099
|
+
console.log(`🎭 Processing duplicate ID ${id}: "${duplicateName}"`);
|
|
1100
|
+
|
|
1101
|
+
let newNameEN: string | null = null;
|
|
1102
|
+
let newNamePT: string | null = null;
|
|
1103
|
+
let attempts = 0;
|
|
1104
|
+
const maxAttempts = 3;
|
|
1105
|
+
|
|
1106
|
+
while (attempts < maxAttempts) {
|
|
1107
|
+
attempts++;
|
|
1108
|
+
|
|
1109
|
+
console.log(
|
|
1110
|
+
`📡 Attempt ${attempts}/${maxAttempts} - Requesting AI for new name...`
|
|
1111
|
+
);
|
|
1112
|
+
|
|
1113
|
+
// ✅ Generate a new name, passing dynamically tracked duplicates
|
|
1114
|
+
const messages = this.context
|
|
1115
|
+
.buildLLMMessages()
|
|
1116
|
+
.regenerateConceptName({
|
|
1117
|
+
conceptSlug,
|
|
1118
|
+
oldNameEN: duplicateName,
|
|
1119
|
+
description,
|
|
1120
|
+
recentNames: Array.from(recentDuplicates),
|
|
1121
|
+
});
|
|
1122
|
+
|
|
1123
|
+
const aiResponse = await this.context
|
|
1124
|
+
.api()
|
|
1125
|
+
.callTogether.single(messages, {});
|
|
1126
|
+
|
|
1127
|
+
if (!aiResponse) {
|
|
1128
|
+
console.warn(
|
|
1129
|
+
`⚠️ AI failed to generate a new name for ID ${id}, skipping.`
|
|
1130
|
+
);
|
|
1131
|
+
break;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
console.log(`📝 Raw AI Response:\n${aiResponse}`);
|
|
1135
|
+
|
|
1136
|
+
// ✅ Mapping for concept names with proper PT articles
|
|
1137
|
+
const conceptMapping = {
|
|
1138
|
+
amulet: { en: 'Amulet', pt: 'Amuleto', articlePT: 'O' },
|
|
1139
|
+
crown: { en: 'Crown', pt: 'Coroa', articlePT: 'A' },
|
|
1140
|
+
scepter: { en: 'Scepter', pt: 'Cetro', articlePT: 'O' },
|
|
1141
|
+
lantern: { en: 'Lantern', pt: 'Candeia', articlePT: 'A' },
|
|
1142
|
+
ring: { en: 'Ring', pt: 'Anel', articlePT: 'O' },
|
|
1143
|
+
orb: { en: 'Orb', pt: 'Orbe', articlePT: 'O' },
|
|
1144
|
+
};
|
|
1145
|
+
|
|
1146
|
+
const {
|
|
1147
|
+
en: conceptEN,
|
|
1148
|
+
pt: conceptPT,
|
|
1149
|
+
articlePT,
|
|
1150
|
+
} = conceptMapping[conceptSlug] || {
|
|
1151
|
+
en: conceptSlug,
|
|
1152
|
+
pt: conceptSlug,
|
|
1153
|
+
articlePT: 'O', // Default to "O" if concept is missing
|
|
1154
|
+
};
|
|
1155
|
+
|
|
1156
|
+
// ✅ Strict regex matching for English and Portuguese
|
|
1157
|
+
const enMatch = aiResponse.match(
|
|
1158
|
+
new RegExp(`EN:\\s*The ${conceptEN} of (.+)`)
|
|
1159
|
+
);
|
|
1160
|
+
const ptMatch = aiResponse.match(
|
|
1161
|
+
new RegExp(`PT:\\s*${articlePT} ${conceptPT} de (.+)`)
|
|
1162
|
+
);
|
|
1163
|
+
|
|
1164
|
+
if (!enMatch || !ptMatch) {
|
|
1165
|
+
console.error(
|
|
1166
|
+
`❌ Invalid AI response format for ID ${id}, retrying...`
|
|
1167
|
+
);
|
|
1168
|
+
continue;
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
newNameEN = `The ${conceptEN} of ${enMatch[1].trim()}`;
|
|
1172
|
+
newNamePT = `${articlePT} ${conceptPT} de ${ptMatch[1].trim()}`;
|
|
1173
|
+
|
|
1174
|
+
console.log(
|
|
1175
|
+
`🎨 Extracted Names: EN - "${newNameEN}", PT - "${newNamePT}"`
|
|
1176
|
+
);
|
|
1177
|
+
|
|
1178
|
+
// ✅ Check if the new name is already in use in D1 before updating
|
|
1179
|
+
const existingEntry = await db
|
|
1180
|
+
.select({ id: conceptsData.id })
|
|
1181
|
+
.from(conceptsData)
|
|
1182
|
+
.where(eq(conceptsData.name, newNameEN))
|
|
1183
|
+
.execute();
|
|
1184
|
+
|
|
1185
|
+
if (existingEntry.length > 0 || recentDuplicates.has(newNameEN)) {
|
|
1186
|
+
console.warn(`⚠️ Duplicate detected: "${newNameEN}", retrying...`);
|
|
1187
|
+
continue;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
recentDuplicates.add(newNameEN);
|
|
1191
|
+
break; // ✅ Found a valid new name
|
|
1099
1192
|
}
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
amulet: { en: "Amulet", pt: "Amuleto", articlePT: "O" },
|
|
1106
|
-
crown: { en: "Crown", pt: "Coroa", articlePT: "A" },
|
|
1107
|
-
scepter: { en: "Scepter", pt: "Cetro", articlePT: "O" },
|
|
1108
|
-
lantern: { en: "Lantern", pt: "Candeia", articlePT: "A" },
|
|
1109
|
-
ring: { en: "Ring", pt: "Anel", articlePT: "O" },
|
|
1110
|
-
orb: { en: "Orb", pt: "Orbe", articlePT: "O" },
|
|
1111
|
-
};
|
|
1112
|
-
|
|
1113
|
-
const { en: conceptEN, pt: conceptPT, articlePT } = conceptMapping[conceptSlug] || {
|
|
1114
|
-
en: conceptSlug,
|
|
1115
|
-
pt: conceptSlug,
|
|
1116
|
-
articlePT: "O", // Default to "O" if concept is missing
|
|
1117
|
-
};
|
|
1118
|
-
|
|
1119
|
-
// ✅ Strict regex matching for English and Portuguese
|
|
1120
|
-
const enMatch = aiResponse.match(new RegExp(`EN:\\s*The ${conceptEN} of (.+)`));
|
|
1121
|
-
const ptMatch = aiResponse.match(new RegExp(`PT:\\s*${articlePT} ${conceptPT} de (.+)`));
|
|
1122
|
-
|
|
1123
|
-
if (!enMatch || !ptMatch) {
|
|
1124
|
-
console.error(`❌ Invalid AI response format for ID ${id}, retrying...`);
|
|
1193
|
+
|
|
1194
|
+
if (!newNameEN || !newNamePT) {
|
|
1195
|
+
console.error(
|
|
1196
|
+
`🚨 Failed to generate a unique name for ID ${id} after ${maxAttempts} attempts.`
|
|
1197
|
+
);
|
|
1125
1198
|
continue;
|
|
1126
1199
|
}
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
.
|
|
1136
|
-
.from(conceptsData)
|
|
1200
|
+
|
|
1201
|
+
console.log(
|
|
1202
|
+
`✅ Updating names: EN - "${newNameEN}", PT - "${newNamePT}"`
|
|
1203
|
+
);
|
|
1204
|
+
|
|
1205
|
+
// ✅ Update English name using the ID
|
|
1206
|
+
await db
|
|
1207
|
+
.update(conceptsData)
|
|
1208
|
+
.set({ name: newNameEN })
|
|
1137
1209
|
.where(
|
|
1138
|
-
and(
|
|
1139
|
-
eq(conceptsData.language, 'en-us'),
|
|
1140
|
-
eq(conceptsData.name, newNameEN),
|
|
1141
|
-
eq(conceptsData.conceptSlug, conceptSlug)
|
|
1142
|
-
)
|
|
1210
|
+
and(eq(conceptsData.id, id), eq(conceptsData.language, 'en-us'))
|
|
1143
1211
|
)
|
|
1144
1212
|
.execute();
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1213
|
+
|
|
1214
|
+
// ✅ Convert English ID to Portuguese ID by replacing `:en-us:` with `:pt-br:`
|
|
1215
|
+
const ptId = id.replace(':en-us:', ':pt-br:');
|
|
1216
|
+
|
|
1217
|
+
// ✅ Update Portuguese name using the derived PT ID
|
|
1218
|
+
await db
|
|
1219
|
+
.update(conceptsData)
|
|
1220
|
+
.set({ name: newNamePT })
|
|
1221
|
+
.where(
|
|
1222
|
+
and(eq(conceptsData.id, ptId), eq(conceptsData.language, 'pt-br'))
|
|
1223
|
+
)
|
|
1224
|
+
.execute();
|
|
1225
|
+
|
|
1226
|
+
console.log(
|
|
1227
|
+
`📝 Updated IDs: EN (${id}) -> "${newNameEN}", PT (${ptId}) -> "${newNamePT}"`
|
|
1228
|
+
);
|
|
1229
|
+
|
|
1230
|
+
updatedEntries++;
|
|
1158
1231
|
}
|
|
1159
|
-
|
|
1160
|
-
console.log(`✅ Updating names: EN - "${newNameEN}", PT - "${newNamePT}"`);
|
|
1161
|
-
|
|
1162
|
-
// ✅ Update English name using the ID
|
|
1163
|
-
await db
|
|
1164
|
-
.update(conceptsData)
|
|
1165
|
-
.set({ name: newNameEN })
|
|
1166
|
-
.where(and(eq(conceptsData.id, id), eq(conceptsData.language, 'en-us')))
|
|
1167
|
-
.execute();
|
|
1168
|
-
|
|
1169
|
-
// ✅ Convert English ID to Portuguese ID by replacing `:en-us:` with `:pt-br:`
|
|
1170
|
-
const ptId = id.replace(':en-us:', ':pt-br:');
|
|
1171
|
-
|
|
1172
|
-
// ✅ Update Portuguese name using the derived PT ID
|
|
1173
|
-
await db
|
|
1174
|
-
.update(conceptsData)
|
|
1175
|
-
.set({ name: newNamePT })
|
|
1176
|
-
.where(and(eq(conceptsData.id, ptId), eq(conceptsData.language, 'pt-br')))
|
|
1177
|
-
.execute();
|
|
1178
|
-
|
|
1179
|
-
console.log(`📝 Updated IDs: EN (${id}) -> "${newNameEN}", PT (${ptId}) -> "${newNamePT}"`);
|
|
1180
|
-
|
|
1181
|
-
updatedEntries++;
|
|
1182
1232
|
}
|
|
1183
|
-
|
|
1184
|
-
console.log(
|
|
1233
|
+
|
|
1234
|
+
console.log(
|
|
1235
|
+
`🎉 [COMPLETE] Regenerated ${updatedEntries} duplicate names for concept: ${conceptSlug}.`
|
|
1236
|
+
);
|
|
1185
1237
|
return {
|
|
1186
1238
|
message: `Duplicate names processed and regenerated for ${conceptSlug}.`,
|
|
1187
1239
|
};
|