Krotos Modules 3
Loading...
Searching...
No Matches
TextToAssets.cpp
Go to the documentation of this file.
1#include "TextToAssets.h"
2
3namespace krotos
4{
5 TextToAssets::TextToAssets() : Thread("TTAThread"), m_statusUpdater(m_listeners), m_progressUpdater(m_listeners)
6 {
7 File ggmlDataFile = File(krotos::utils::StringsIntoPath(AssetManager::getPluginDirectory().getFullPathName(),
8 "ttpResources", modelFileName));
9
10 if (ggmlDataFile.existsAsFile())
11 {
13 }
14
15 startThread();
16 }
17
19 {
20 try
21 {
22 stopThread(4000);
23 }
24 catch (const std::exception& e)
25 {
26 // Handle exception a
27 Logger::writeToLog("Error stopping thread: " + String(e.what()));
28 }
29 }
30
31 StringArray TextToAssets::parseAnthropicResponse(std::unique_ptr<InputStream>& stream)
32 {
33 if (!stream)
34 {
35 throw AnthropicAPIException("Invalid stream from Anthropic API");
36 }
37
38 auto result = stream->readEntireStreamAsString();
39 auto json = JSON::parse(result);
40
41 if (json.isVoid())
42 {
43 throw AnthropicAPIException("Error parsing JSON returned by Anthropic API");
44 }
45
46 var content = json.getProperty(contentIdentifier, var());
47 if (!content.isArray() || content.size() != 1)
48 {
49 throw AnthropicAPIException("Unexpected content structure in JSON returned by Anthropic API");
50 }
51
52 auto text = content[0].getProperty(textIdentifier, var()).toString();
53 auto jsonText = JSON::parse(text);
54
55 if (jsonText.isVoid())
56 {
57 throw LLMJsonParseException("Error parsing JSON returned by LLM");
58 }
59
60 // Optimized layer parsing for JUCE StringArray
61 StringArray layers;
62 layers.ensureStorageAllocated(4); // Pre-allocate space for 4 layers
63
64 static const std::array<Identifier, 4> layerIdentifiers = {layer1Identifier, layer2Identifier, layer3Identifier,
66
67 for (const auto& identifier : layerIdentifiers)
68 {
69 auto layerValue = jsonText.getProperty(identifier, var()).toString();
70 if (layerValue.isEmpty())
71 {
72 throw LLMJsonParseException("Missing or empty layer in LLM response: " + identifier.toString());
73 }
74 layers.add(layerValue);
75 }
76
77 return layers;
78 }
79
80 StringArray TextToAssets::getLLMLayers(std::unique_ptr<InputStream>& stream)
81 {
82 StringArray layers;
83
84 try
85 {
86 layers = parseAnthropicResponse(stream);
87 // Process layers
88 for (int i = 0; i < layers.size(); ++i)
89 {
90 DBG("LLM Layer " + String(i + 1) + ": " + layers[i]);
91 }
92 }
93 catch (const AnthropicAPIException& e)
94 {
95 // Log to Sentry: Chris b0rked the Anthropic API
96 DBG("Anthropic API Error: " + String(e.what()));
97 // Report to Sentry here
98 }
99 catch (const LLMJsonParseException& e)
100 {
101 // Log to Sentry: LLM JSON parsing error from Chris
102 DBG("LLM JSON Parse Error: " + String(e.what()));
103 // Report to Sentry here
104 }
105 catch (const std::exception& e)
106 {
107 // Log to Sentry: Any other unexpected error
108 DBG("Unexpected Error: " + String(e.what()));
109 // Report to Sentry here
110 }
111
112 return layers;
113 }
114
115 StringArray TextToAssets::anthropicAPICall(String query)
116 {
117 URL url("https://api.anthropic.com/v1/messages");
118
119 // Decrypt the Claude API key
120 const int ivLength{32};
121 const int claudeApiKeyLength{108};
122 // Output vectors for converted decryption input arguments
123 std::vector<unsigned char> ivBytes, ciphertextBytes, encryptionKeyBytes;
124 // Output vector for the decrypted key plaintext
125 std::vector<unsigned char> plaintextBytes;
126 // The encryption key used to encrypt the Claude API key in hex format.
127 const std::string encKey("23c3b67142161bb753706f4673f234906a811e10cf434e832790a4f120ab90e1");
128
129 // Extract the IV and ciphertext
130 auto ivHex = m_claudeApiKeyEncrypted.substring(0, ivLength).toStdString();
131 auto ciphertextHex = m_claudeApiKeyEncrypted.substring(ivLength).toStdString();
132
133 // Convert from hex to byte array which can be passed to EVP
134 try
135 {
136 utils::hexStringToByteArray(ivHex, ivBytes);
137 utils::hexStringToByteArray(ciphertextHex, ciphertextBytes);
138 utils::hexStringToByteArray(encKey, encryptionKeyBytes);
139 }
140 catch (const std::invalid_argument& e)
141 {
142 DBG("TextToAssets: Failed to open Claude API Key\n\t" + String(e.what()));
143 jassertfalse;
144 return StringArray();
145 }
146
147 // Perform the decrypt operation
148 EVP_CIPHER_CTX* cipherContext;
149 int outputLen;
150 int plaintextLen;
151
152 plaintextBytes.resize(ciphertextBytes.size());
153
154 // Create and initialise the context
155 if (!(cipherContext = EVP_CIPHER_CTX_new()))
156 {
157 DBG("TextToAssets: Could not create new EVP context");
158 }
159
160 // Initialize the decryption operation
161 if (EVP_DecryptInit_ex(cipherContext, EVP_aes_256_cbc(), NULL, encryptionKeyBytes.data(), ivBytes.data()) != 1)
162 {
163 DBG("TextToAssets: Could not initialise EVP context with the given parameters");
164 }
165
166 // Perform decryption
167 if (EVP_DecryptUpdate(cipherContext, plaintextBytes.data(), &outputLen, ciphertextBytes.data(),
168 ciphertextBytes.size()) != 1)
169 {
170 DBG("TextToAssets: Could not perform decryption");
171 }
172 plaintextLen = outputLen;
173
174 // Finalize decryption
175 if (EVP_DecryptFinal_ex(cipherContext, plaintextBytes.data() + outputLen, &outputLen) != 1)
176 {
177 DBG("TextToAssets: Could not finalise decryption");
178 }
179 plaintextLen += outputLen;
180
181 // Clean up
182 EVP_CIPHER_CTX_free(cipherContext);
183
184 // Key bytes to string
185 std::string apiKey(plaintextBytes.begin(), plaintextBytes.end());
186 apiKey = apiKey.substr(0, claudeApiKeyLength);
187
188 // Construct the request body with the conversation format
189 String requestBody = R"({
190 "model": "claude-3-haiku-20240307",
191 "max_tokens": 1024,
192 "temperature": 0,
193 "system": "Your task is to design an ambience preset for a given soundscape.\nAmbience presets should contain 4 layers.\nEach layer should contribute something unique and meaningful to the preset while being relevent to the requested soundscape.\nExample layers: Birds, Wind, Rain, Stream, Crickets, Animal calls, Waterfall, Campfire, Thunder\nHere is an example ambience preset in JSON format:\n{\"Soundscape\": \"Forest\", \"Layer-1\": \"Birds\", \"Layer-2\": \"Wind\", \"Layer-3\": \"Crickets\", \"Layer-4\": \"Stream\"}\nReturn the designed preset in JSON format and no other output.",
194 "messages": [
195 {"role": "user", "content": "{query}"}
196 ]
197 })";
198
199 // swap out placeholder {query} with the actual user query
200 requestBody = requestBody.replace("{query}", query);
201 url = url.withPOSTData(requestBody);
202
203 // create a stream for the response
204 // TODO: https://krotosltd.atlassian.net/browse/KST-2608
205 StringPairArray responseHeaders;
206 int statusCode = 0;
207 auto stream = url.createInputStream(
208 URL::InputStreamOptions(URL::ParameterHandling::inAddress)
209 .withHttpRequestCmd("POST")
210 .withExtraHeaders({"x-api-key: " + apiKey +
211 "\nanthropic-version: 2023-06-01\ncontent-type: application/json"})
212 .withConnectionTimeoutMs(10000)
213 .withResponseHeaders(&responseHeaders)
214 .withStatusCode(&statusCode));
215
216 auto layers = getLLMLayers(stream);
217 return layers;
218 }
219
221 {
222 // load json from binary data
223 const char* jsonBinaryData = reinterpret_cast<const char*>(KrotosBinaryData::preset_layers_json);
224
225 // convert to string
226 String jsonString(jsonBinaryData);
227
228 // and parse it
229 auto parsedData = JSON::parse(jsonString);
230
231 if (!parsedData.isVoid())
232 {
233 const auto presetLayersIdentifier = Identifier("presetLayers");
234 const auto parentCatIDIdentifier = Identifier("parentCatID");
235 const auto collectionIdentifier = Identifier("collection");
236 const auto layerNameIdentifier = Identifier("layerName");
237 const auto layerDescriptionIdentifier = Identifier("layerDescription");
238 const auto assetCatIDIdentifier = Identifier("assetCatID");
239 const auto assetDescriptionIdentifier = Identifier("assetDescription");
240 const auto engineIdentifier = Identifier("engine");
241 const auto layerEmbeddingIdentifier = Identifier("layerEmbedding");
242 const auto assetEmbeddingIdentifier = Identifier("assetEmbedding");
243
244 var result = parsedData.getProperty(presetLayersIdentifier, 0);
245 for (int i = 0; i < result.size(); ++i)
246 {
247 Layer layer;
248 layer.isValid = true;
249 layer.bias = 0.0f;
250 layer.parentCatID = result[i].getProperty(parentCatIDIdentifier, 0).toString();
251 layer.collection = result[i].getProperty(collectionIdentifier, 0).toString();
252 layer.layerName = result[i].getProperty(layerNameIdentifier, 0).toString();
253 layer.layerDescription = result[i].getProperty(layerDescriptionIdentifier, 0).toString();
254 layer.assetCatID = result[i].getProperty(assetCatIDIdentifier, 0).toString();
255 layer.assetDescription = result[i].getProperty(assetDescriptionIdentifier, 0).toString();
256 layer.engine = result[i].getProperty(engineIdentifier, 0).toString();
257
258 // layer embedding
259 auto layerEmbeddingArray = result[i].getProperty(layerEmbeddingIdentifier, 0);
260 layer.layerEmbedding.resize(layerEmbeddingArray.size(), 0.0f);
261 for (int j = 0; j < layerEmbeddingArray.size(); ++j)
262 {
263 layer.layerEmbedding[j] = layerEmbeddingArray[j];
264 }
265
266 // asset embedding
267 auto assetEmbeddingArray = result[i].getProperty(assetEmbeddingIdentifier, 0);
268 layer.assetEmbedding.resize(assetEmbeddingArray.size(), 0.0f);
269 for (int j = 0; j < assetEmbeddingArray.size(); ++j)
270 {
271 layer.assetEmbedding[j] = assetEmbeddingArray[j];
272 }
273
274 // store the layers grouped by parent CatID (e.g. keep ambience forest layers together)
275 m_layersMap[layer.parentCatID].push_back(layer);
276 }
277 }
278 else
279 {
280 DBG("Failed to load KrotosBinaryData::preset_layers_json");
281 jassertfalse;
282 }
283 }
284
286 {
287 // load json from binary data
288 const char* jsonBinaryData = reinterpret_cast<const char*>(KrotosBinaryData::query_embeddings_json);
289
290 // convert to string
291 String jsonString(jsonBinaryData);
292
293 // and parse it
294 auto parsedData = JSON::parse(jsonString);
295
296 if (!parsedData.isVoid())
297 {
298 const auto queryEmbeddingsIdentifier = Identifier("queryEmbeddings");
299 const auto catIDIdentifier = Identifier("catID");
300 const auto embeddingIdentifier = Identifier("embedding");
301
302 var result = parsedData.getProperty(queryEmbeddingsIdentifier, 0);
303 m_queryEmbeddings.reserve(result.size());
304 for (int i = 0; i < result.size(); ++i)
305 {
306 auto catID = result[i].getProperty(catIDIdentifier, 0).toString();
307 auto embeddingArray = result[i].getProperty(embeddingIdentifier, 0);
308 std::vector<float> embedding(embeddingArray.size(), 0.0f);
309 for (int j = 0; j < embeddingArray.size(); ++j)
310 {
311 embedding[j] = embeddingArray[j];
312 }
313 m_queryEmbeddings.push_back(std::make_pair(catID, embedding));
314 }
315 }
316 else
317 {
318 DBG("Failed to load KrotosBinaryData::query_embeddings_json");
319 jassertfalse;
320 }
321 }
322
323 std::tuple<String, String> TextToAssets::findClosestAmbienceCatID(const String& query,
324 const std::vector<float>& queryEmbedding)
325 {
326 if (m_queryEmbeddings.empty())
327 {
328 return {"", ""};
329 }
330
331 // TODO: exclude ambience subcategories in a separate method
332 // forest and tropical subcategories easily confused, so discount tropical unless keyword detected
333 bool isTropical = query.contains("tropical");
334
335 String CatID = "";
336 auto maxScore = std::numeric_limits<float>::lowest();
337 std::vector<float> maxEmbedding(queryEmbedding.size(), 0.0f);
338 for (const auto& element : m_queryEmbeddings)
339 {
340 const auto name = element.first;
341 if ((!isTropical) && (name == "AMBTrop"))
342 {
343 continue;
344 }
345
346 const auto embedding = element.second;
347 const auto score = dot(embedding, queryEmbedding);
348 if (score >= maxScore)
349 {
350 CatID = name;
351 maxScore = score;
352 maxEmbedding = embedding;
353 }
354 }
355
356 // check if a combination of two ambience subcategories significantly improves the score
357 String CatID2 = "";
358 float maxScore2 = maxScore;
359 for (const auto& element : m_queryEmbeddings)
360 {
361 const auto name = element.first;
362 if ((!isTropical) && (name == "AMBTrop"))
363 {
364 continue;
365 }
366
367 // sum the current embedding with the maxEmbedding we found earlier.
368 // normalise the combined embedding and calculate the similarity score against the query.
369 auto embedding = element.second;
370 std::transform(embedding.begin(), embedding.end(), maxEmbedding.begin(), embedding.begin(), std::plus<>{});
371 normalise(embedding);
372 const auto score = dot(embedding, queryEmbedding);
373 if (score >= maxScore2)
374 {
375 CatID2 = name;
376 maxScore2 = score;
377 }
378 }
379
380 /* KST-2686 - it is possible for a category to be chosen where the layer which will be added contains a sample
381 * not within the embeddings/on the users system, so deal with that here */
382 if (m_datasetMap.count(CatID2) == 0)
383 {
384 return {CatID, ""};
385 }
386
387 constexpr float threshold = 0.1f;
388 const auto isValid = maxScore2 >= (maxScore + threshold) ? true : false;
389 CatID2 = isValid ? CatID2 : "";
390
391 return {CatID, CatID2};
392 }
393
394 std::vector<int> TextToAssets::argsort(const std::vector<float>& x) const
395 {
396 std::vector<int> indices(x.size());
397 std::iota(indices.begin(), indices.end(), 0);
398 std::sort(indices.begin(), indices.end(), [&x](int left, int right) -> bool { return x[left] > x[right]; });
399 return indices;
400 }
401
402 std::tuple<float, int, bool> TextToAssets::findLayer(const std::vector<float>& queryEmbedding,
403 const std::vector<float>& presetEmbedding,
404 const std::vector<Layer>& layers,
405 const std::vector<float>& layerScoresLLM)
406 {
407 auto layerScore = std::numeric_limits<float>::lowest();
408 int layerIdx = 0;
409 bool isValid = false;
410 for (int i = 0; i < (int)layers.size(); ++i)
411 {
412 auto layer = layers[i];
413 if (!layer.isValid)
414 {
415 continue;
416 }
417
418 isValid = true; // we found at least one valid layer
419 auto embedding = layer.layerEmbedding;
420
421 // sum preset embedding with current layer embeddings (the preset embedding)
422 std::transform(embedding.begin(), embedding.end(), presetEmbedding.begin(), embedding.begin(),
423 std::plus<>{});
424
425 // normalise embedding so dot product will give us cosine similarity
426 normalise(embedding);
427
428 // this is comparing query and combined preset embedding
429 auto simScore = dot(embedding, queryEmbedding);
430
431 // use LLM scores to bias the layer selection
432 // e.g. the query "forest storm" gives generated names "wind", "thunder", "rain" and "rustling leaves"
433 // our layers with similar names will have higher similarity scores, and become more likely to be selected
434 // we use a weighted score as we still care about how the combined preset embedding matches the query
435
436 // combine score with match against LLM generated layers
437 simScore = 0.5f * simScore + 0.5f * layerScoresLLM[i];
438
439 if (simScore >= layerScore)
440 {
441 layerScore = simScore;
442 layerIdx = i;
443 }
444 }
445
446 return {layerScore, layerIdx, isValid};
447 }
448
449 std::tuple<String, float> TextToAssets::findAsset(const Layer& layer)
450 {
451 const auto catID = layer.assetCatID;
452 const auto description = layer.assetDescription;
453 const auto embedding = layer.assetEmbedding;
454
455 if (m_datasetMap.count(catID) == 0)
456 {
457 return {"", 0.0f}; // no assets under that CatID
458 }
459
460 // cosine similarity between layer embedding and file embeddings
461 const auto vectorScores = getVectorScores(catID, embedding);
462
463 // BM25 scores for layer label and filenames
464 const auto ids = m_datasetIDMap[catID];
465 const auto bm25Scores = m_bm25->getBatchScores(description, ids);
466
467 // use Reciprocal Rank Fusion (RRF) to combine embedding scores and BM25 scores
468 const auto rrfScores = calculateReciprocalRankFusion(vectorScores, bm25Scores);
469
470 auto maxScore = std::numeric_limits<float>::lowest();
471 std::size_t maxIdx = 0;
472 auto cosineScore = std::numeric_limits<float>::lowest();
473 for (std::size_t i = 0; i < ids.size(); ++i)
474 {
475 auto score = rrfScores[i];
476 if (score >= maxScore)
477 {
478 maxScore = score;
479 maxIdx = i;
480 cosineScore = vectorScores[i];
481 }
482 }
483 const auto path = m_datasetMap[catID][maxIdx].path;
484 return {path, cosineScore};
485 }
486
487 bool TextToAssets::containsKeyword(const String& text, const StringArray& keywords)
488 {
489 for (const auto& keyword : keywords)
490 {
491 if (text.contains(keyword))
492 {
493 return true;
494 }
495 }
496 return false;
497 }
498
499 void TextToAssets::maskLayersContainingTag(String tag, std::vector<Layer>& layers)
500 {
501 std::for_each(layers.begin(), layers.end(), [tag](Layer& layer) {
502 layer.isValid = (layer.assetCatID.contains(tag)) ? false : layer.isValid;
503 });
504 }
505
506 String TextToAssets::capitalize(const String& text)
507 {
508 return text.substring(0, 1).toUpperCase() + text.substring(1).toLowerCase();
509 }
510
511 std::vector<float> TextToAssets::calculateLLMLayerScores(const std::vector<Layer>& layers,
512 const StringArray& generatedLayerNames)
513 {
514 ScopedLock sl(m_cs);
515
516 std::vector<std::vector<float>> embeddings;
517 embeddings.reserve(generatedLayerNames.size());
518 for (const auto& name : generatedLayerNames)
519 {
520 auto embedding = m_sentenceTransformer.encode(name.toStdString());
521 embeddings.push_back(embedding);
522 }
523
524 std::vector<float> layerScoresLLM;
525 layerScoresLLM.reserve(layers.size());
526 for (const auto& layer : layers)
527 {
528 const auto layerEmbedding = layer.layerEmbedding;
529 const auto layerBias = layer.bias;
530 float maxScore = std::numeric_limits<float>::lowest();
531 for (std::size_t i = 0; i < generatedLayerNames.size(); ++i)
532 {
533 auto score = dot(embeddings[i], layerEmbedding) + layerBias;
534 maxScore = std::max(maxScore, score);
535 }
536 layerScoresLLM.push_back(maxScore);
537 }
538 return layerScoresLLM;
539 }
540
541 void TextToAssets::addBiasToLayers(const String& query, std::vector<Layer>& layers)
542 {
543 // Map of collections to their associated keywords
544 const std::unordered_map<String, std::vector<String>> collectionKeywords = {
545 {"aeroplane interior", {"aeroplane interior", "airplane interior", "commercial flight"}},
546 {"airport", {"airport"}},
547 {"apartment", {"apartment"}},
548 {"basement", {"basement"}},
549 {"bathroom", {"bathroom"}},
550 {"beach", {"beach", "seaside"}},
551 {"bus interior", {"bus interior"}},
552 {"city", {"city", "urban"}},
553 {"construction", {"construction", "building site"}},
554 {"countryside", {"countryside", "rural"}},
555 {"desert", {"desert"}},
556 {"forest", {"forest", "woodland"}},
557 {"garage", {"garage"}},
558 {"hospital", {"hospital"}},
559 {"hotel", {"hotel"}},
560 {"kitchen", {"kitchen"}},
561 {"office", {"office", "workplace"}},
562 {"park", {"park"}},
563 {"restaurant", {"restaurant", "cafe"}},
564 {"sci-fi", {"starship", "spaceship"}},
565 {"sewer", {"sewer"}},
566 {"suburban", {"suburban", "suburbs"}},
567 {"subway", {"subway"}},
568 {"swamp", {"swamp", "marsh"}},
569 {"town", {"town", "village"}},
570 {"underwater", {"underwater"}}};
571
572 // Vector to store detected collections
573 std::vector<String> detectedCollections;
574
575 // Check for keywords in the query that match specific KS preset keywords
576 for (const auto& [collection, keywords] : collectionKeywords)
577 {
578 // 'collection' is a structured binding so it must be copied to a variable for lambda capture
579 auto currentCollection = collection;
580 bool collectionDetected =
581 std::any_of(keywords.begin(), keywords.end(), [&query, &currentCollection](const String& keyword) {
582 // Special case for "desert": check for whole word
583 bool isDetected = (currentCollection == "desert") ? query.containsWholeWord(keyword)
584 : query.contains(keyword);
585 if (isDetected)
586 {
587 // Log detected collections
588 DBG("[collections] " + currentCollection);
589 }
590 return isDetected;
591 });
592 if (collectionDetected)
593 {
594 detectedCollections.push_back(collection);
595 }
596 }
597
598 // Use a non-zero bias for layers in any detected collections
599 // The bias is added to the cosine similarity score during layer selection
600 // This makes layers in a collection more likely to be selected
601 // e.g. a query of "quiet office" will be more likely to select layers assigned to office
602 for (auto& layer : layers)
603 {
604 if (std::find(detectedCollections.begin(), detectedCollections.end(), layer.collection) !=
605 detectedCollections.end())
606 {
607 // We want the bias to be just large enough to influence layer selection
608 // but not so large that relevant layers outside the collection get excluded
609 layer.bias = 0.25f;
610 }
611 }
612 }
613
614 std::tuple<String, std::array<TextToAssets::TTPAsset, 4>> TextToAssets::findPresetFiles(String searchTerm)
615 {
616 ScopedLock sl(m_cs);
617
618 // init results
619 std::array<TTPAsset, 4> results;
620
621 // this threshold is used to determine if we found a suitable asset
622 constexpr float cosineSimilarityThreshold = 0.5f;
623
624 // minimal preprocessing of the user query
625 auto [query, excludeList] = preprocessQuery(searchTerm);
626
627 // compute the SBERT embedding for the query
628 auto queryEmbedding = m_sentenceTransformer.encode(query.toStdString());
629
630 // classification using nearest neighbour
631 auto [closestCatID, secondaryCatID] = findClosestAmbienceCatID(query, queryEmbedding);
632 if (closestCatID.isEmpty())
633 {
634 return {"", results};
635 }
636 auto categorySubCategory = m_UCS->getCategorySubCategory(closestCatID);
637
638 DBG("[closestCatID] " << closestCatID);
639 DBG("[secondaryCatID] " << secondaryCatID);
640
641 std::vector<Layer> layers;
642
643 if (isStrictMode())
644 {
645 // use a subset of T2P layers based on classifying query to closest matching ambience subcategory
646 auto layers = m_layersMap[closestCatID];
647 if (layers.empty())
648 {
649 return {categorySubCategory, results};
650 }
651
652 // add additional layers
653 if (secondaryCatID.isNotEmpty())
654 {
655 const auto secondaryLayers = m_layersMap[secondaryCatID];
656 layers.insert(layers.end(), secondaryLayers.begin(), secondaryLayers.end());
657 }
658
659 // print available layers
660 for (const auto& layer : layers)
661 {
662 DBG(layer.layerName << ": " << layer.layerDescription);
663 }
664 }
665 else
666 {
667 // use all available T2P layers
668 for (auto [categoryName, categoryLayers] : m_layersMap)
669 {
670 layers.insert(layers.end(), categoryLayers.begin(), categoryLayers.end());
671 }
672
673 // add non-zero bias to relevant layers if the query matches specific KS presets
674 addBiasToLayers(query, layers);
675 }
676
677 // -----------------------------------
678 // layer names generated by an LLM
679 // -----------------------------------
680 auto generatedLayerNames = anthropicAPICall(searchTerm);
681 if (generatedLayerNames.size() != 4)
682 {
683 DBG("LLM generation failed");
684 return {"", results};
685 }
686
687 // score our layers based on the generated LLM names
688 auto layerScoresLLM = calculateLLMLayerScores(layers, generatedLayerNames);
689 // -----------------------------------
690
691 // initially all the layers should be valid
692 std::for_each(layers.begin(), layers.end(), [](Layer& layer) { layer.isValid = true; });
693
694 // exclude layers based on query containing -tag
695 excludeLayers(excludeList, layers);
696
697 // initialise the preset embedding to zeros
698 std::vector<float> presetEmbedding(queryEmbedding.size(), 0.0f);
699
700 // we'll store paths, labels and core engine mode for each layer in the preset
701 std::set<String> uniqueAssets;
702 const int maximumSearchIterations = 20;
703 std::size_t resultsIndex = 0;
704 for (int i = 0; i < maximumSearchIterations; ++i)
705 {
706 // find a layer from the valid layers
707 auto [layerScore, layerIdx, layerValid] =
708 findLayer(queryEmbedding, presetEmbedding, layers, layerScoresLLM);
709 if (!layerValid)
710 {
711 // we didn't find a single suitable layer
712 break;
713 }
714 auto layer = layers[layerIdx];
715 auto name = layer.layerName;
716
717 // set layers with the same name as invalid so we don't end up selecting duplicates
718 std::for_each(layers.begin(), layers.end(),
719 [name](Layer& layer) { layer.isValid = (layer.layerName == name) ? false : layer.isValid; });
720
721 // find an asset for the selected layer
722 auto [asset, assetScore] = findAsset(layer);
723
724 // check for issues with the selected asset
725 if (assetScore < cosineSimilarityThreshold)
726 {
727 // we didn't find a suitable local asset - continue searching
728 DBG("missing local asset for layer with name: " << name);
729 continue;
730 }
731 else if (uniqueAssets.contains(asset))
732 {
733 // we already have a preset layer that uses this asset
734 DBG("skipping asset as already selected: " << asset);
735 continue;
736 }
737
738 // sum the selected layer embedding with previous ones
739 std::transform(presetEmbedding.begin(), presetEmbedding.end(), layer.layerEmbedding.begin(),
740 presetEmbedding.begin(), std::plus<>{});
741
742 // store the layer result
743 results[resultsIndex].path = asset;
744 results[resultsIndex].label = capitalize(name);
745 results[resultsIndex].engine = layer.engine;
746
747 // insert asset into set to check for duplicates
748 uniqueAssets.insert(asset);
749
750 // increment index
751 ++resultsIndex;
752
753 // check if we're done
754 if (resultsIndex == results.size())
755 {
756 break;
757 }
758 }
759
760 return {categorySubCategory, results};
761 }
762
763 bool TextToAssets::isUCSValid(String catID)
764 {
765 jassert(m_UCS != nullptr);
766 return m_UCS->isValid(catID);
767 }
768
769 void TextToAssets::run()
770 {
771 if (!threadShouldExit())
772 {
773 loadQueryEmbeddings(); // embeddings used to classify the query into an ambience subcategory
774 loadPresetLayers(); // sets of named preset layers for supported ambience subcategory
775 loadFileEmbeddings();
776 }
777 }
778
779 File TextToAssets::createFilePath(String filename)
780 {
781 return utils::StringsIntoPath(AssetManager::getPluginDirectory().getFullPathName(), "ttpResources", filename);
782 }
783
784 bool TextToAssets::appendDataToEmbeddingsFile()
785 {
786 // clear asset counter
787 m_assetCounter.clear();
788
789 // clear BM25 corpus vector
790 m_bm25Corpus.clear();
791
792 // load existing embeddings
793 auto embeddingsFile = createFilePath(ttfEmbeddingsFileName);
794
795 std::unordered_map<String, std::vector<Data>> datasetMap;
796
797 if (embeddingsFile.existsAsFile())
798 {
799 auto json = JSON::parse(embeddingsFile);
800 var result = json.getProperty(FilesEmbeddingsIdentifier, 0);
801 for (int i = 0; i < result.size(); ++i)
802 {
803 auto fileName = result[i].getProperty(fileNameIdentifier, 0).toString();
804 auto catID = result[i].getProperty(catIDNameIdentifier, 0).toString();
805 var embeddingsArray = result[i].getProperty(ttfEmbeddingsNameIdentifier, 0);
806
807 std::vector<float> embeddings;
808 for (int j = 0; j < embeddingsArray.size(); ++j)
809 {
810 embeddings.push_back(embeddingsArray[j]);
811 }
812
813 Data data(catID, fileName, embeddings);
814 datasetMap[catID].push_back(data);
815 }
816 }
817 else
818 {
819 embeddingsFile.create();
820 }
821
822 // append new ones to the data
823 for (auto const& [key, dataset] : m_datasetMap)
824 {
825 for (const auto& element : dataset)
826 {
827 datasetMap[key].push_back(element);
828 }
829 }
830
831 // write the new dict to the embeddings file
832 DynamicObject* datasetObject = new DynamicObject();
833 var datasetObjectVar(datasetObject);
834 Array<var> rows;
835
836 for (auto const& [key, dataset] : datasetMap)
837 {
838 for (const auto& element : dataset)
839 {
840 const auto fileName = element.path;
841 const auto catID = element.catID;
842 const auto embeddings = element.embeddings;
843
844 DynamicObject* metadata = new DynamicObject();
845 var metadataVar(metadata);
846 metadata->setProperty(fileNameIdentifier, var(fileName));
847
848 metadata->setProperty(catIDNameIdentifier, var(catID));
849
850 Array<var> embeddingsArray;
851 for (auto value : embeddings)
852 {
853 embeddingsArray.add(value);
854 }
855 metadata->setProperty(ttfEmbeddingsNameIdentifier, embeddingsArray);
856
857 rows.add(metadata);
858 metadata = nullptr;
859 }
860 }
861 datasetObject->setProperty(FilesEmbeddingsIdentifier, rows);
862
863 // need to have all data loaded and updated in order to perform search
864 m_datasetMap = datasetMap;
865
866 TemporaryFile tempEmbeddingsFile(embeddingsFile, TemporaryFile::useHiddenFile);
867 if (auto stream = tempEmbeddingsFile.getFile().createOutputStream())
868 {
869 // overwrite an existing file
870 stream->setPosition(0);
871 stream->truncate();
872 JSON::writeToStream(*stream, datasetObject);
873 stream->flush();
874
875 // close the output stream, so we can switch out the temp file.
876 stream.reset();
877
878 const bool success = tempEmbeddingsFile.overwriteTargetFileWithTemporary();
879
880 if (!success)
881 {
882 // Failed to overwrite!
883 jassertfalse;
884 return false;
885 }
886 }
887 else
888 {
889 return false;
890 }
891
892 readFromFile(embeddingsFile);
893
894 return true;
895 }
896
897 void TextToAssets::loadFileEmbeddings()
898 {
899 ScopedLock sl(m_cs);
900 // load tags/embeddings if available
901 auto path = createFilePath(ttfEmbeddingsFileName);
902 if (path.existsAsFile())
903 {
904 // clear previously stored data
905 readFromFile(path);
906 }
907 }
908
909 String TextToAssets::sanitizeString(String text)
910 {
911 text = text.replace("_", " ");
912 text = text.replace("RXd", "");
913 if (text.contains("SNDBTS"))
914 {
915 text = text.upToFirstOccurrenceOf("SNDBTS", false, false);
916 }
917 text = text.removeCharacters("0123456789");
918 text = text.trim();
919 text = text.toLowerCase();
920 return text;
921 }
922
923 bool TextToAssets::readFromFile(File file)
924 {
925 if (file.existsAsFile() == false)
926 {
927 return false;
928 }
929
930 clear();
931
932 auto json = JSON::parse(file);
933 var result = json.getProperty(FilesEmbeddingsIdentifier, 0);
934 auto resultSize = result.size();
935 for (int i = 0; i < resultSize; ++i)
936 {
937 auto fileName = result[i].getProperty(fileNameIdentifier, 0).toString();
938 auto catID = result[i].getProperty(catIDNameIdentifier, 0).toString();
939 var embeddingsArray = result[i].getProperty(ttfEmbeddingsNameIdentifier, 0);
940
941 std::vector<float> embeddings;
942 for (int j = 0; j < embeddingsArray.size(); ++j)
943 {
944 embeddings.push_back(embeddingsArray[j]);
945 }
946
947 auto name = File(fileName).getFileNameWithoutExtension();
948 name = name.replace(catID, "");
949 name = sanitizeString(name);
950 m_datasetIDMap[catID].push_back(m_bm25Corpus.size());
951 m_bm25Corpus.push_back(name);
952 ++m_assetCounter[catID];
953
954 Data data(catID, fileName, embeddings);
955 m_datasetMap[catID].push_back(data);
956 }
957
958 // init BM25 class so we can rank files
959 if (!m_bm25Corpus.empty())
960 {
961 m_bm25 = std::make_unique<BM25>(m_bm25Corpus);
962 }
963
964 return true;
965 }
966
967 void TextToAssets::clear()
968 {
969 m_datasetMap.clear();
970 m_datasetIDMap.clear();
971 m_assetCounter.clear();
972 m_bm25Corpus.clear();
973 m_samplePaths.clear();
974 m_sampleFilenames.clear();
975 }
976
977 bool TextToAssets::processFileList(Array<File>& filesToProcess)
978 {
979 if (!isModelFileAvailable())
980 {
981 return false;
982 }
983 // lock
984 ScopedLock sl(m_cs);
985
986 notifyAvailabilityStatus(false);
987
988 // clear previously stored data
989 clear();
990
991 // Time estimation variables
992 const int totalFiles = filesToProcess.size();
993 int processedFiles = 0;
994 const Time startTime = Time::getCurrentTime();
995 Time lastUpdateTime = startTime;
996
997 for (File& file : filesToProcess)
998 {
999 calculateEmbeddingsForFile(file);
1000 if (Thread::currentThreadShouldExit())
1001 {
1002 clear();
1003
1004 // Notify processing status update change
1005 notifyAvailabilityStatus(true);
1006 return false;
1007 }
1008
1009 // Update time estimation
1010 ++processedFiles;
1011 const Time currentTime = Time::getCurrentTime();
1012 const RelativeTime elapsedTime = currentTime - startTime;
1013 const RelativeTime timeSinceLastUpdate = currentTime - lastUpdateTime;
1014
1015 // Check if 500ms have passed since the last update
1016 if (timeSinceLastUpdate.inMilliseconds() >= 500)
1017 {
1018 const double averageTimePerFile = elapsedTime.inSeconds() / processedFiles;
1019 const int remainingFiles = totalFiles - processedFiles;
1020 const double estimatedRemainingSeconds = averageTimePerFile * remainingFiles;
1021
1022 // Convert estimated remaining time to a readable string
1023 const String estimatedTimeRemaining = RelativeTime(estimatedRemainingSeconds).getDescription();
1024
1025 // Notify listeners of progress and estimated time
1026 // Todo: KST-2723 This has been disabled due to memory corruption crashing issues. Reinstate once fixed.
1027 // notifyProgressUpdate(processedFiles, totalFiles, estimatedTimeRemaining);
1028
1029 // Update the last update time
1030 lastUpdateTime = currentTime;
1031 }
1032 }
1033
1034 // init BM25 class so we can rank files
1035 if (!m_bm25Corpus.empty())
1036 {
1037 m_bm25 = std::make_unique<BM25>(m_bm25Corpus);
1038 }
1039
1040 notifyAvailabilityStatus(true);
1041
1042 // append - save embeddings to file
1043 auto appendSuccessful = appendDataToEmbeddingsFile();
1044 return appendSuccessful;
1045 }
1046
1047 void TextToAssets::calculateEmbeddingsForFile(const File& file)
1048 {
1049 ScopedLock sl(m_cs);
1050
1051 if (m_UCS == nullptr)
1052 {
1053 return;
1054 }
1055
1056 auto name = file.getFileNameWithoutExtension();
1057 auto catID = name.upToFirstOccurrenceOf("_", false, false);
1058 catID = (catID == "AMBROOM") ? "AMBRoom" : catID; // a fix for basement preset assets
1059 catID = (catID == "AMBUrb") ? "AMBUrbn" : catID; // a fix for skatepark preset assets
1060 const auto valid = isUCSValid(catID);
1061 jassert(valid); // we should already have filtered out invalid files
1062
1063 // TODO: ideally kaf files would include metadata
1064 const auto sampleRate = 48000;
1065 const auto bitDepth = 16;
1066 const auto channelCount = 2;
1067 const auto durationSeconds = 5; // TODO: determine suitable minimum file duration
1068 const auto bytesThreshold = (bitDepth / 8) * channelCount * (durationSeconds * sampleRate);
1069 auto sizeInBytes = file.getSize(); // proxy for duration
1070 if (sizeInBytes >= bytesThreshold)
1071 {
1072 name = name.replace(catID, ""); // remove leading UCS CatID tag
1073 name = sanitizeString(name);
1074 auto embedding = m_sentenceTransformer.encode(name.toStdString());
1075
1076 // update dataset
1077 Data data(catID, file.getFullPathName().toStdString(), embedding);
1078 m_datasetMap[catID].push_back(data);
1079
1080 // update asset counter
1081 ++m_assetCounter[catID];
1082
1083 // update BM25 docs
1084 m_datasetIDMap[catID].push_back(m_bm25Corpus.size());
1085 m_bm25Corpus.push_back(name);
1086 }
1087 }
1088
1089 bool TextToAssets::matchesCategory(const String& catID, StringArray tags) const
1090 {
1091 for (auto tag : tags)
1092 {
1093 if (catID.startsWith(tag))
1094 return true;
1095 }
1096 return false;
1097 }
1098
1099 std::vector<float> TextToAssets::rankify(const std::vector<float>& x) const
1100 {
1101 const auto n = (int)x.size();
1102
1103 // rank vector
1104 std::vector<float> R(n, 0);
1105
1106 std::vector<std::pair<float, int>> T(n);
1107 for (int i = 0; i < n; i++)
1108 {
1109 T[i] = std::make_pair(x[i], i);
1110 }
1111 // Sort T descending according to first element
1112 std::sort(T.begin(), T.end(),
1113 [](const std::pair<float, int>& a, const std::pair<float, int>& b) { return a.first > b.first; });
1114
1115 float rank = 1.0f, m = 1.0f, i = 0.0f;
1116 while (i < n)
1117 {
1118 float j = i;
1119 // Get no of elements with equal rank
1120 while (j < n - 1 && T[j].first == T[j + 1].first)
1121 {
1122 j += 1;
1123 }
1124
1125 m = j - i + 1;
1126
1127 for (int k = 0; k < m; ++k)
1128 {
1129 // For each equal element use formula
1130 // obtain index of T[i+j][0] in A
1131 int idx = T[i + k].second;
1132 R[idx] = (double)(rank + (m - 1) * 0.5);
1133 }
1134
1135 // Increment rank and i
1136 rank += m;
1137 i += m;
1138 }
1139 return R;
1140 }
1141
1142 std::vector<std::pair<float, int>> TextToAssets::topk(std::vector<std::pair<float, int>> data, int topk) const
1143 {
1144 // compute the top-k
1145 topk = std::min(topk, (int)data.size());
1146 std::nth_element(
1147 data.begin(), data.begin() + topk, data.end(),
1148 [](const std::pair<float, int>& a, const std::pair<float, int>& b) { return a.first > b.first; });
1149 data.resize(topk);
1150 return data;
1151 }
1152
1153 std::vector<float> TextToAssets::calculateReciprocalRankFusion(const std::vector<float>& scores1,
1154 const std::vector<float>& scores2, float k,
1155 float alpha) const
1156 {
1157 const auto ranks1 = rankify(scores1);
1158 const auto ranks2 = rankify(scores2);
1159 std::vector<float> scores;
1160 for (int i = 0; i < (int)ranks1.size(); ++i)
1161 {
1162 const auto score1 = 1.0f / (ranks1[i] + k);
1163 const auto score2 = 1.0f / (ranks2[i] + k);
1164 const auto score = alpha * score1 + (1.0f - alpha) * score2;
1165 scores.push_back(score);
1166 }
1167 return scores;
1168 }
1169
1170 std::tuple<String, StringArray> TextToAssets::preprocessQuery(String query) const
1171 {
1172 StringArray queryTokens;
1173 queryTokens.addTokens(query.toLowerCase(), " ", "\"");
1174
1175 // update exclude tags so we can filter out files that contain those keywords
1176 StringArray excludeList; // {"designed", "misc"}; // initial exclude tags
1177 query = "";
1178 for (const auto& token : queryTokens)
1179 {
1180 if (token.startsWithChar('-'))
1181 {
1182 excludeList.addIfNotAlreadyThere(token.replaceSection(0, 1, ""));
1183 }
1184 else
1185 {
1186 query += " ";
1187 query += token;
1188 }
1189 }
1190 return {query, excludeList};
1191 }
1192
1193 void TextToAssets::excludeLayers(const StringArray& excludeList, std::vector<Layer>& layers)
1194 {
1195 for (auto& layer : layers)
1196 {
1197 const auto name = layer.layerName;
1198 for (const auto& word : excludeList)
1199 {
1200 if (name.containsWholeWord(word))
1201 {
1202 layer.isValid = false;
1203 break;
1204 }
1205 }
1206 }
1207 }
1208
1209 float TextToAssets::dot(const std::vector<float>& a, const std::vector<float>& b) const
1210 {
1211 return std::inner_product(a.begin(), a.end(), b.begin(), 0.0f);
1212 }
1213
1214 void TextToAssets::normalise(std::vector<float>& x) const
1215 {
1216 const auto scale = 1.0f / std::max(std::sqrt(dot(x, x)), 1e-12f);
1217 for (int i = 0; i < x.size(); ++i)
1218 {
1219 x[i] *= scale;
1220 }
1221 }
1222
1223 std::vector<float> TextToAssets::getVectorScores(String catID, const std::vector<float>& x) const
1224 {
1225 std::vector<float> vectorScores;
1226 if (m_datasetMap.count(catID) > 0)
1227 {
1228 const auto dataset = m_datasetMap.at(catID);
1229 vectorScores.reserve(dataset.size());
1230 for (const auto& element : dataset)
1231 {
1232 auto emb = element.embeddings;
1233 auto score = dot(x, emb);
1234 vectorScores.push_back(score);
1235 }
1236 }
1237 return vectorScores;
1238 }
1239
1240 void TextToAssets::searchForTagAndProperty(ValueTree& root, Identifier tag, Identifier property, String newSample,
1241 Identifier target)
1242 {
1243 int index = 0;
1244 ValueTree child;
1245 int numChildrean = root.getNumChildren();
1246
1247 while ((child = root.getChild(index)).isValid())
1248 {
1249 if (child.hasType(tag) && child.getParent().getParent().getType().toString() == "KWIDGET" &&
1250 child.getParent().getParent().getProperty("id").toString() == target.toString())
1251 {
1252 if (child.getProperty(Identifier("id")).toString() == property.toString())
1253 {
1254 // replace with new sample paths
1255 child.setProperty("value", newSample, nullptr);
1256
1257 return;
1258 }
1259 else
1260 {
1261 searchForTagAndProperty(child, tag, property, newSample, target);
1262 }
1263 }
1264 else
1265 {
1266 searchForTagAndProperty(child, tag, property, newSample, target);
1267 }
1268 index++;
1269 }
1270 return;
1271 }
1272
1273 void TextToAssets::updateTabLabel(ValueTree& root, int index, const String& label)
1274 {
1275 // Find CUSTOMDATA node
1276 ValueTree customDataNode = root.getChildWithName(Identifier("CUSTOMDATA"));
1277
1278 if (customDataNode.isValid())
1279 {
1280 // Find C_E_LABELS node within CUSTOMDATA
1281 ValueTree labelsNode = customDataNode.getChildWithName(Identifier("C_E_LABELS"));
1282 if (labelsNode.isValid())
1283 {
1284 // Update the specific TABLABEL element by index
1285 for (int i = 0; i < labelsNode.getNumChildren(); ++i)
1286 {
1287 ValueTree tabLabelNode = labelsNode.getChild(i);
1288 if (tabLabelNode.hasType(Identifier("TABLABEL")) && tabLabelNode.getProperty("index") == var(index))
1289 {
1290 tabLabelNode.setProperty("text", label, nullptr);
1291 return;
1292 }
1293 }
1294 }
1295 }
1296
1297 // If the TABLABEL with the given index was not found
1298 jassertfalse;
1299 DBG("Could not update the TABLABEL: specified index not found.");
1300 }
1301
1302 void TextToAssets::updateCoreEngineParamValue(ValueTree& tree, const var& coreEngineID, const var& paramID,
1303 const String& value)
1304 {
1305 // [1] update CoreEngine param value
1306 auto kwidgets = tree.getChildWithName(Identifier("KWIDGETS"));
1307 jassert(kwidgets.isValid());
1308
1309 auto kwidget = kwidgets.getChildWithProperty(Identifier("id"), coreEngineID);
1310 jassert(kwidget.isValid());
1311
1312 auto params = kwidget.getChildWithName(Identifier("PARAMS"));
1313 jassert(params.isValid());
1314
1315 auto param = params.getChildWithProperty(Identifier("id"), paramID);
1316 jassert(param.isValid() && param.hasProperty(Identifier("value")));
1317
1318 param.setProperty("value", value, nullptr);
1319
1320 // [2] update param with matching linkIndex
1321 auto linkIndex = param.getPropertyAsValue(Identifier("linkIndex"), nullptr).toString();
1322 linkIndex = "#" + linkIndex.paddedLeft('0', 3); // e.g. "#052"
1323
1324 params = tree.getChildWithName(Identifier("PARAMS"));
1325 jassert(params.isValid());
1326
1327 param = params.getChildWithProperty(Identifier("id"), linkIndex);
1328 jassert(param.isValid() && param.hasProperty(Identifier("value")));
1329
1330 param.setProperty("value", value, nullptr);
1331 }
1332
1333 std::unique_ptr<XmlElement> TextToAssets::getInitTTPData()
1334 {
1335 auto xml = parseXML(KrotosBinaryData::init_ttp_preset_xml);
1336 return xml;
1337 }
1338
1339 void TextToAssets::notifyAvailabilityStatus(bool isAvailable)
1340 {
1341 m_ttaAvailable = isAvailable;
1342 if (m_synchronousUpdates)
1343 {
1344 m_listeners.call([this](Listener& l) { l.onTextToAssetsProcessingStatusChanged(m_ttaAvailable); });
1345 }
1346 else
1347 {
1348 m_statusUpdater.triggerAsyncUpdate(
1349 [this](Listener& l) { l.onTextToAssetsProcessingStatusChanged(m_ttaAvailable); });
1350 }
1351 }
1352
1353 void TextToAssets::notifyProgressUpdate(const int processedFiles, const int totalFiles,
1354 const String& estimatedTimeRemaining)
1355 {
1356 if (m_synchronousUpdates)
1357 {
1358 m_listeners.call([processedFiles, totalFiles, estimatedTimeRemaining](Listener& l) {
1359 l.onTextToAssetsProgressUpdate(processedFiles, totalFiles, estimatedTimeRemaining);
1360 });
1361 }
1362 else
1363 {
1364 m_progressUpdater.triggerAsyncUpdate([processedFiles, totalFiles, estimatedTimeRemaining](Listener& l) {
1365 l.onTextToAssetsProgressUpdate(processedFiles, totalFiles, estimatedTimeRemaining);
1366 });
1367 }
1368 }
1369
1370 //==============================================================================
1371 void TextToAssets::addListener(Listener* listener) { m_listeners.add(listener); }
1372
1373 void TextToAssets::removeListener(Listener* listener) { m_listeners.remove(listener); }
1374
1375} // namespace krotos
Definition TextToAssets.h:399
static File getPluginDirectory()
Definition AssetManager.cpp:392
Definition TextToAssets.h:404
std::vector< float > encode(std::string sentence) const
Definition SentenceTransformer.cpp:49
Listener for the TextToAssets module.
Definition TextToAssets.h:123
virtual void onTextToAssetsProcessingStatusChanged(bool isAvailable)=0
called when the text to assets module has finished processing files
virtual void onTextToAssetsProgressUpdate(int processedFiles, int totalFiles, const String &estimatedTimeRemaining)=0
Callback to notify progress updates on file processing.
std::vector< std::pair< String, std::vector< float > > > m_queryEmbeddings
Definition TextToAssets.h:374
~TextToAssets()
Definition TextToAssets.cpp:18
String capitalize(const String &text)
make first letter of string upper case and the rest lower case
SentenceTransformer m_sentenceTransformer
Definition TextToAssets.h:362
std::tuple< float, int, bool > findLayer(const std::vector< float > &queryEmbedding, const std::vector< float > &presetEmbedding, const std::vector< Layer > &layers, const std::vector< float > &layerScoresLLM)
find a relevant preset layer
String m_claudeApiKeyEncrypted
Definition TextToAssets.h:379
void maskLayersContainingTag(String tag, std::vector< Layer > &layers)
set layer.isValid to false for layers containing tag.
std::vector< float > calculateLLMLayerScores(const std::vector< Layer > &layers, const StringArray &generatedLayerNames)
return similarity scores for layers based on the LLM generated layer names
std::unordered_map< String, std::vector< std::size_t > > m_datasetIDMap
Definition TextToAssets.h:347
CriticalSection m_cs
Definition TextToAssets.h:368
void normalise(std::vector< float > &x) const
L2 normalise the input vector.
Definition TextToAssets.cpp:1214
float dot(const std::vector< float > &a, const std::vector< float > &b) const
return dot product between a and b
Definition TextToAssets.cpp:1209
void addBiasToLayers(const String &query, std::vector< Layer > &layers)
add a non-zero bias to layers that match certain keywords
std::tuple< String, String > findClosestAmbienceCatID(const String &query, const std::vector< float > &queryEmbedding)
search for closest match to query
StringArray getLLMLayers(std::unique_ptr< InputStream > &stream)
returns a StringArray of preset layer names generated by an LLM
Definition TextToAssets.cpp:80
std::vector< float > calculateReciprocalRankFusion(const std::vector< float > &scores1, const std::vector< float > &scores2, float k=2.0f, float alpha=0.5f) const
calculate the reciprocal rank fusion for scores1 and scores2
StringArray anthropicAPICall(String query)
send query to an LLM using the anthropic API
Definition TextToAssets.cpp:115
StringArray parseAnthropicResponse(std::unique_ptr< InputStream > &stream)
parse Anthropic API response and return results in a StringArray
Definition TextToAssets.cpp:31
bool m_modelFileAvailable
Definition TextToAssets.h:182
std::unordered_map< String, std::vector< Layer > > m_layersMap
Definition TextToAssets.h:377
std::vector< float > getVectorScores(String catID, const std::vector< float > &x) const
return vector scores between query embedding and dataset embeddings
Definition TextToAssets.cpp:1223
bool containsKeyword(const String &text, const StringArray &keywords)
returns true if the text contains any keyword in keywords.
std::unique_ptr< BM25 > m_bm25
Definition TextToAssets.h:340
std::vector< int > argsort(const std::vector< float > &x) const
returns the indices that would sort the vector.
std::unordered_map< String, std::vector< Data > > m_datasetMap
Definition TextToAssets.h:346
void loadPresetLayers()
load preset layers from json stored as binary data
std::tuple< String, float > findAsset(const Layer &layer)
find a relevant asset for a given preset layer
void loadQueryEmbeddings()
load quer embeddings from json stored as binary data
TextToAssets()
Definition TextToAssets.cpp:5
void hexStringToByteArray(const std::string &input, std::vector< unsigned char > &byteArray)
Converts a hex string to a byte array.
Definition helpers.cpp:148
String StringsIntoPath(Args... args)
Joins multiple string arguments into a path string.
Definition helpers.h:25
Definition AirAbsorptionFilter.cpp:2
static const Identifier layer3Identifier
Definition TextToAssets.h:25
static const Identifier layer1Identifier
Definition TextToAssets.h:23
static const Identifier layer4Identifier
Definition TextToAssets.h:26
static const Identifier contentIdentifier
Definition TextToAssets.h:20
constexpr char modelFileName[]
Definition SentenceTransformer.h:7
static const Identifier textIdentifier
Definition TextToAssets.h:21
static const Identifier layer2Identifier
Definition TextToAssets.h:24