C++ - Utils
Ce document présente les classes d'outils offertes par AIChatPlus, comprenant des fonctionnalités telles que la manipulation JSON, le traitement d'images et le traitement audio.
Préparation
Ajoutez les dépendances de module dans le fichier .Build.cs du projet :
Inclure les fichiers d'en-tête nécessaires :
#include "Common/AIChatPlus_Util.h"
#include "Common/Json/AIChatPlus_JsonObject.h"
#include "Common/Json/AIChatPlus_JsonArray.h"
Opérations JSON
AIChatPlus propose une classe de manipulation JSON compatible avec Blueprint, prenant en charge les appels en chaîne et les opérations de type sécurisé.
UAIChatPlus_JsonObject - Objet JSON
Création et analyse
#include "Common/Json/AIChatPlus_JsonObject.h"
void UMyClass::JsonObjectBasics()
{
// Créer un objet JSON vide
UAIChatPlus_JsonObject* JsonObj = UAIChatPlus_JsonObject::Create();
// À partir d'une chaîne de caractères analysée
FString JsonString = TEXT(R"({"name": "John", "age": 30, "active": true})");
bool bSuccess;
FString ErrorMessage;
UAIChatPlus_JsonObject* ParsedObj = UAIChatPlus_JsonObject::Parse(JsonString, bSuccess, ErrorMessage);
if (bSuccess)
{
UE_LOG(LogTemp, Display, TEXT("Parsed successfully"));
}
else
{
UE_LOG(LogTemp, Error, TEXT("Parse error: %s"), *ErrorMessage);
}
}
Définir les champs (appel chaîné)
void UMyClass::SetJsonFields()
{
UAIChatPlus_JsonObject* JsonObj = UAIChatPlus_JsonObject::Create();
// Configuration en chaîne des champs
JsonObj->SetStringField(TEXT("name"), TEXT("John"))
->SetIntegerField(TEXT("age"), 30)
->SetBooleanField(TEXT("active"), true)
->SetNumberField(TEXT("score"), 95.5f)
->SetNullField(TEXT("extra"));
// Définir l'objet imbriqué
UAIChatPlus_JsonObject* AddressObj = UAIChatPlus_JsonObject::Create();
AddressObj->SetStringField(TEXT("city"), TEXT("Tokyo"))
->SetStringField(TEXT("country"), TEXT("Japan"));
JsonObj->SetObjectField(TEXT("address"), AddressObj);
// sortie
FString Result = JsonObj->ToString(true); // true = sortie formatée
UE_LOG(LogTemp, Display, TEXT("%s"), *Result);
}
Obtenir le champ
void UMyClass::GetJsonFields()
{
FString JsonString = TEXT(R"({"name": "John", "age": 30, "scores": [85, 90, 95]})");
bool bParseSuccess;
FString ErrorMessage;
UAIChatPlus_JsonObject* JsonObj = UAIChatPlus_JsonObject::Parse(JsonString, bParseSuccess, ErrorMessage);
if (!bParseSuccess) return;
bool bSuccess;
// Obtenir un champ de chaîne de caractères
FString Name = JsonObj->GetStringField(TEXT("name"), TEXT("Unknown"), bSuccess);
if (bSuccess)
{
UE_LOG(LogTemp, Display, TEXT("Name: %s"), *Name);
}
// Obtenir le champ entier
int32 Age = JsonObj->GetIntegerField(TEXT("age"), 0, bSuccess);
// Obtenir un champ booléen
bool bActive = JsonObj->GetBooleanField(TEXT("active"), false, bSuccess);
// Obtenir le champ tableau
UAIChatPlus_JsonArray* Scores = JsonObj->GetArrayField(TEXT("scores"), bSuccess);
if (bSuccess && Scores)
{
for (int32 i = 0; i < Scores->Length(); ++i)
{
int32 Score = Scores->GetInteger(i, 0, bSuccess);
UE_LOG(LogTemp, Display, TEXT("Score[%d]: %d"), i, Score);
}
}
// Obtenir un champ d'objet imbriqué
UAIChatPlus_JsonObject* AddressObj = JsonObj->GetObjectField(TEXT("address"), bSuccess);
if (bSuccess && AddressObj)
{
FString City = AddressObj->GetStringField(TEXT("city"), TEXT(""), bSuccess);
}
}
Vérification et gestion des champs
void UMyClass::ManageJsonFields()
{
UAIChatPlus_JsonObject* JsonObj = UAIChatPlus_JsonObject::Create();
JsonObj->SetStringField(TEXT("name"), TEXT("John"))
->SetIntegerField(TEXT("age"), 30);
// Vérifier si le champ existe
if (JsonObj->HasField(TEXT("name")))
{
UE_LOG(LogTemp, Display, TEXT("Field 'name' exists"));
}
// Obtenir le type de champ
EAIChatPlus_JsonValueType FieldType = JsonObj->GetFieldType(TEXT("age"));
// FieldType == EAIChatPlus_JsonValueType::Number
// Obtenir tous les noms de champs
TArray<FString> FieldNames = JsonObj->GetFieldNames();
for (const FString& Name : FieldNames)
{
UE_LOG(LogTemp, Display, TEXT("Field: %s"), *Name);
}
// Supprimer le champ
JsonObj->RemoveField(TEXT("age"));
// Vider tous les champs
JsonObj->Clear();
}
Requête de chemin (accès imbriqué)
void UMyClass::PathQuery()
{
FString JsonString = TEXT(R"({
"player": {
"inventory": {
"gold": 1000,
"items": ["sword", "shield"]
}
}
})");
bool bParseSuccess;
FString ErrorMessage;
UAIChatPlus_JsonObject* JsonObj = UAIChatPlus_JsonObject::Parse(JsonString, bParseSuccess, ErrorMessage);
// Obtenir une valeur par le chemin
FAIChatPlus_JsonQueryResult Result;
FString Gold = JsonObj->GetStringByPath(TEXT("player.inventory.gold"), TEXT("0"), Result);
// Gold = "1000"
// Définir la valeur par chemin
FAIChatPlus_JsonPathOptions PathOptions;
JsonObj->SetStringByPath(TEXT("player.inventory.gold"), TEXT("2000"), PathOptions);
}
Fusion et duplication
void UMyClass::MergeAndDuplicate()
{
UAIChatPlus_JsonObject* Obj1 = UAIChatPlus_JsonObject::Create();
Obj1->SetStringField(TEXT("name"), TEXT("John"));
UAIChatPlus_JsonObject* Obj2 = UAIChatPlus_JsonObject::Create();
Obj2->SetIntegerField(TEXT("age"), 30)
->SetStringField(TEXT("name"), TEXT("Jane")); // Écraser name
// Fusionner (les champs existants sont écrasés si bOverwrite = true)
Obj1->Merge(Obj2, true);
// Obj1 contient maintenant {"name": "Jane", "age": 30}
// Copie profonde
UAIChatPlus_JsonObject* Copy = Obj1->Duplicate();
}
Convertible avec UStruct
USTRUCT(BlueprintType)
struct FMyData
{
GENERATED_BODY()
UPROPERTY()
FString Name;
UPROPERTY()
int32 Age;
};
void UMyClass::StructConversion()
{
// Conversion de UStruct en JsonObject
FMyData Data;
Data.Name = TEXT("John");
Data.Age = 30;
UAIChatPlus_JsonObject* JsonObj = UAIChatPlus_JsonObject::FromStruct(Data);
// Conversion de JsonObject en UStruct
FMyData OutData;
bool bSuccess = JsonObj->ToStruct(OutData);
if (bSuccess)
{
UE_LOG(LogTemp, Display, TEXT("Name: %s, Age: %d"), *OutData.Name, OutData.Age);
}
}
UAIChatPlus_JsonArray - Tableau JSON
Création et manipulation
#include "Common/Json/AIChatPlus_JsonArray.h"
void UMyClass::JsonArrayBasics()
{
// Créer un tableau vide
UAIChatPlus_JsonArray* JsonArray = UAIChatPlus_JsonArray::Create();
// Ajouter un élément (appel chaîné)
JsonArray->AddString(TEXT("apple"))
->AddString(TEXT("banana"))
->AddInteger(42)
->AddBoolean(true)
->AddNull();
// Ajouter un élément objet
UAIChatPlus_JsonObject* ItemObj = UAIChatPlus_JsonObject::Create();
ItemObj->SetStringField(TEXT("name"), TEXT("sword"));
JsonArray->AddObject(ItemObj);
// Ajouter un tableau imbriqué
UAIChatPlus_JsonArray* NestedArray = UAIChatPlus_JsonArray::Create();
NestedArray->AddInteger(1)->AddInteger(2)->AddInteger(3);
JsonArray->AddArray(NestedArray);
// sortie
FString Result = JsonArray->ToString(true);
UE_LOG(LogTemp, Display, TEXT("%s"), *Result);
}
Obtenir un élément
void UMyClass::GetArrayElements()
{
FString JsonString = TEXT(R"(["apple", "banana", 42, true, {"name": "item"}])");
bool bParseSuccess;
FString ErrorMessage;
UAIChatPlus_JsonArray* JsonArray = UAIChatPlus_JsonArray::Parse(JsonString, bParseSuccess, ErrorMessage);
if (!bParseSuccess) return;
bool bSuccess;
// Obtenir l'élément de chaîne
FString Fruit = JsonArray->GetString(0, TEXT(""), bSuccess); // "apple"
// Obtenir l'élément entier
int32 Number = JsonArray->GetInteger(2, 0, bSuccess); // 42
// Obtenir l'élément booléen
bool bValue = JsonArray->GetBoolean(3, false, bSuccess); // true
// Obtenir les éléments de l'objet
UAIChatPlus_JsonObject* Obj = JsonArray->GetObject(4, bSuccess);
if (bSuccess && Obj)
{
FString Name = Obj->GetStringField(TEXT("name"), TEXT(""), bSuccess);
}
// Parcourir le tableau
for (int32 i = 0; i < JsonArray->Length(); ++i)
{
EAIChatPlus_JsonValueType Type = JsonArray->GetElementType(i);
UE_LOG(LogTemp, Display, TEXT("Element[%d] type: %d"), i, static_cast<int32>(Type));
}
}
Manipulation de tableaux
void UMyClass::ArrayOperations()
{
UAIChatPlus_JsonArray* JsonArray = UAIChatPlus_JsonArray::Create();
JsonArray->AddString(TEXT("a"))
->AddString(TEXT("b"))
->AddString(TEXT("c"));
// obtenir la longueur
int32 Len = JsonArray->Length(); // 3
// Vérifier si l'indice est valide
bool bValid = JsonArray->IsValidIndex(1); // true
// Définir l'élément
bool bSuccess;
JsonArray->SetString(1, TEXT("B"), bSuccess);
// Supprimer un élément
JsonArray->RemoveAt(0, bSuccess);
// Vider le tableau
JsonArray->Clear();
// Copie profonde
UAIChatPlus_JsonArray* Copy = JsonArray->Duplicate();
}
Conversion par lot
void UMyClass::BatchConversion()
{
// Création à partir d'un tableau de chaînes
TArray<FString> StringArray = {TEXT("a"), TEXT("b"), TEXT("c")};
UAIChatPlus_JsonArray* JsonArray = UAIChatPlus_JsonArray::FromStringArray(StringArray);
// Reconversion en tableau de chaînes de caractères
bool bSuccess;
TArray<FString> OutArray = JsonArray->ToStringArray(bSuccess);
// Création à partir d'un tableau d'entiers
TArray<int32> IntArray = {1, 2, 3, 4, 5};
UAIChatPlus_JsonArray* IntJsonArray = UAIChatPlus_JsonArray::FromIntegerArray(IntArray);
// Reconversion en tableau d'entiers
TArray<int32> OutIntArray = IntJsonArray->ToIntegerArray(bSuccess);
}
UAIChatPlus_Util - Fonctions auxiliaires JSON
void UMyClass::JsonUtilFunctions()
{
// Fusionner deux chaînes JSON
FString Json1 = TEXT(R"({"name": "John"})");
FString Json2 = TEXT(R"({"age": 30, "name": "Jane"})");
FString Merged = UAIChatPlus_Util::MergeJsonObjects(Json1, Json2);
// Merged = {"name": "Jane", "age": 30}
// Charger une chaîne JSON en tant qu'objet
TSharedPtr<FJsonObject> JsonObj = UAIChatPlus_Util::LoadJsonString(Json1);
// Conversion d'objet en chaîne de caractères
FString JsonString = UAIChatPlus_Util::ToJsonString(JsonObj);
}
Outil d'image
Chargement et sauvegarde des images
#include "Common/AIChatPlus_Util.h"
void UMyClass::ImageLoadSave()
{
// Chargement de l'image depuis un fichier
FString ImagePath = FPaths::ProjectContentDir() / TEXT("Images/sample.png");
UTexture2D* Texture = UAIChatPlus_Util::LoadImage(ImagePath, true); // true = compilation terminée
if (Texture)
{
UE_LOG(LogTemp, Display, TEXT("Image loaded: %dx%d"),
Texture->GetSizeX(), Texture->GetSizeY());
}
// Enregistrer l'image dans un fichier
FString SavePath = FPaths::ProjectSavedDir() / TEXT("output.png");
bool bSaved = UAIChatPlus_Util::SaveImage(Texture, SavePath);
if (bSaved)
{
UE_LOG(LogTemp, Display, TEXT("Image saved to: %s"), *SavePath);
}
}
Convertir l'image en Base64
void UMyClass::ImageToBase64()
{
UTexture2D* Texture = UAIChatPlus_Util::LoadImage(TEXT("D:/image.png"));
// Convertir en chaîne Base64
// InQuality : 0 = PNG, 1-100 = qualité JPEG
FString B64String = UAIChatPlus_Util::ImageToB64(Texture, 0); // Format PNG
UE_LOG(LogTemp, Display, TEXT("Base64 length: %d"), B64String.Len());
}
Copier l'image
void UMyClass::CopyTexture()
{
UTexture2D* Original = UAIChatPlus_Util::LoadImage(TEXT("D:/image.png"));
// Copier la texture
UTexture2D* Copy = UAIChatPlus_Util::CopyTexture2D(
Original,
nullptr, // Outer (nullptr = GetTransientPackage())
NAME_None, // Nom
RF_NoFlags // Drapeaux
);
// Version Blueprint (avec drapeau par défaut)
UTexture2D* BPCopy = UAIChatPlus_Util::CopyTexture2DInBlueprint(Original);
}
Obtenir une image à partir d'une URL
void UMyClass::GetImageFromUrl()
{
FString ImageUrl = TEXT("https://example.com/image.png");
UAIChatPlus_Util::GetImageFromUrl(ImageUrl,
UAIChatPlus_Util::OnImageCreated::CreateLambda([](UTexture2D* Texture, const FString& Error)
{
if (Texture)
{
UE_LOG(LogTemp, Display, TEXT("Image downloaded: %dx%d"),
Texture->GetSizeX(), Texture->GetSizeY());
}
else
{
UE_LOG(LogTemp, Error, TEXT("Download failed: %s"), *Error);
}
}));
}
Obtenir une image à partir de Base64
void UMyClass::GetImageFromBase64()
{
FString B64String = TEXT("iVBORw0KGgoAAAANSUhEUg..."); // Données en Base64
UAIChatPlus_Util::GetImageFromB64(B64String,
UAIChatPlus_Util::OnImageCreated::CreateLambda([](UTexture2D* Texture, const FString& Error)
{
if (Texture)
{
UE_LOG(LogTemp, Display, TEXT("Image decoded successfully"));
}
else
{
UE_LOG(LogTemp, Error, TEXT("Decode failed: %s"), *Error);
}
}));
}
Copier l'image dans le presse-papiers
void UMyClass::CopyToClipboard()
{
UTexture2D* Texture = UAIChatPlus_Util::LoadImage(TEXT("D:/image.png"));
// Vérifie si c'est pris en charge
if (UAIChatPlus_Util::IsCanCopyTexture2DToClipboard())
{
UAIChatPlus_Util::CopyTexture2DToClipboard(Texture);
UE_LOG(LogTemp, Display, TEXT("Image copied to clipboard"));
}
}
Outils audio
Chargement du fichier WAV
void UMyClass::LoadWavFile()
{
FString WavPath = FPaths::ProjectContentDir() / TEXT("Audio/sample.wav");
USoundWave* Sound = UAIChatPlus_Util::LoadSoundWav(WavPath);
if (Sound)
{
UE_LOG(LogTemp, Display, TEXT("Sound loaded: Duration=%.2f, Channels=%d, SampleRate=%d"),
Sound->Duration,
Sound->NumChannels,
Sound->GetSampleRateForCurrentPlatform());
}
}
Enregistrer l'audio en fichier WAV
void UMyClass::SaveWavFile()
{
// Supposons que Sound est un USoundWave existant
USoundWave* Sound = /* obtenir l'audio */;
FString SavePath = FPaths::ProjectSavedDir() / TEXT("output.wav");
bool bSaved = UAIChatPlus_Util::SaveSoundWav(Sound, SavePath);
if (bSaved)
{
UE_LOG(LogTemp, Display, TEXT("Sound saved to: %s"), *SavePath);
}
}
Audio vers Base64
void UMyClass::SoundToBase64()
{
USoundWave* Sound = UAIChatPlus_Util::LoadSoundWav(TEXT("D:/audio.wav"));
// Convertir en chaîne Base64
FString B64String = UAIChatPlus_Util::SoundToB64(Sound);
UE_LOG(LogTemp, Display, TEXT("Audio Base64 length: %d"), B64String.Len());
}
Conversion des données WAV en SoundWave
void UMyClass::WavDataToSound()
{
// Lire les données brutes depuis un fichier
TArray<uint8> RawData;
FFileHelper::LoadFileToArray(RawData, TEXT("D:/audio.wav"));
// Convertir en SoundWave
USoundWave* Sound = UAIChatPlus_Util::WavDataToSoundWave(
RawData,
false, // bIsAmbiX
false // bIsFuMa
);
if (Sound)
{
UE_LOG(LogTemp, Display, TEXT("Sound created from data"));
}
}
Copier SoundWave
void UMyClass::CopySoundWave()
{
USoundWave* Original = UAIChatPlus_Util::LoadSoundWav(TEXT("D:/audio.wav"));
// Copier l'audio
USoundWave* Copy = UAIChatPlus_Util::CopySoundWave(
Original,
nullptr, // Outer
NAME_None // Nom
);
}
Obtenir les données PCM brutes
void UMyClass::GetPCMData()
{
USoundWave* Sound = UAIChatPlus_Util::LoadSoundWav(TEXT("D:/audio.wav"));
// Obtenir les données PCM brutes
TArray<uint8> PCMData = UAIChatPlus_Util::GetSoundWavePCMData(Sound);
UE_LOG(LogTemp, Display, TEXT("PCM data size: %d bytes"), PCMData.Num());
}
Créer un SoundWave à partir des données d'enregistrement
void UMyClass::RecorderDataToSound()
{
// Supposons les données audio capturées depuis Audio Capture
TArray<float> DonnéesEnregistrées; // Données d'échantillonnage audio
int32 NumChannels = 1; // Mono
int32 SampleRate = 16000; // Taux d'échantillonnage
USoundWave* Sound = UAIChatPlus_Util::RecorderDataToSoundWave(
RecorderData,
NumChannels,
SampleRate
);
if (Sound)
{
UE_LOG(LogTemp, Display, TEXT("Sound created from recorder data"));
}
}
Journalisation des contrôles
void UMyClass::ControlLogging()
{
// Définir le niveau de journalisation interne
UAIChatPlus_Util::SetInternalLogVerbosity(EAIChatPlus_LogVerbosityType::Verbose);
// Niveaux disponibles :
// - NoLogging
// - Fatal
// - Error
// - Warning
// - Display
// - Log
// - Verbose
// - VeryVerbose
}
Fonctions utilitaires de wrapper de réponse
Lors du traitement de la réponse dans le rappel, il est nécessaire de convertir FAIChatPlus_PointerWrapper en un type spécifique :
void UMyClass::HandleCallbackResponse()
{
// Dans le rappel OnFinished
Request->OnFinishedListeners.AddLambda([](const FAIChatPlus_PointerWrapper& ResponseWrapper)
{
// Obtenir le message de réponse
FString Message = UAIChatPlus_Util::GetResponseWrapperMessage(ResponseWrapper);
UE_LOG(LogTemp, Display, TEXT("Message: %s"), *Message);
// ou convertir en type de réponse de base
FAIChatPlus_ChatResponseBodyBase& Response = UAIChatPlus_Util::CastWrapperToResponse(ResponseWrapper);
});
// Dans le rappel OnFailed
Request->OnFailedListeners.AddLambda([](const FAIChatPlus_PointerWrapper& ErrorWrapper)
{
// obtenir la description de l'erreur
FString ErrorDesc = UAIChatPlus_Util::GetErrorWrapperDescription(ErrorWrapper);
UE_LOG(LogTemp, Error, TEXT("Error: %s"), *ErrorDesc);
// ou converti en type d'erreur
FAIChatPlus_ResponseErrorBase& Error = UAIChatPlus_Util::CastWrapperToError(ErrorWrapper);
});
}
Demande d'informations sur le modèle
void UMyClass::QueryModelInfo()
{
// Obtenir la liste des modèles par défaut d'OpenAI
const TArray<FName>& OpenAIModels = UAIChatPlus_Util::GetOpenAIChatDefaultModels();
// Obtenir des informations spécifiques sur le modèle
FAIChatPlus_ChatModelInfo ModelInfo = UAIChatPlus_Util::GetOpenAIChatModelInfo(TEXT("gpt-4o"));
UE_LOG(LogTemp, Display, TEXT("Model: %s, MaxTokens: %d, SupportImage: %s"),
*ModelInfo.Name.ToString(),
ModelInfo.MaxTokens,
ModelInfo.IsSupportSendImage ? TEXT("Yes") : TEXT("No"));
// Modèle Claude
const TArray<FName>& ClaudeModels = UAIChatPlus_Util::GetClaudeChatDefaultModels();
FAIChatPlus_ChatModelInfo ClaudeInfo = UAIChatPlus_Util::GetClaudeChatModelInfo(TEXT("claude-3-5-sonnet"));
// Modèle Gemini
const TArray<FName>& GeminiModels = UAIChatPlus_Util::GetGeminiChatDefaultModels();
FAIChatPlus_ChatModelInfo GeminiInfo = UAIChatPlus_Util::GetGeminiChatModelInfo(TEXT("gemini-1.5-pro"));
}
Fonctions auxiliaires de Cllama
void UMyClass::CllamaUtilities()
{
// Vérifier si Cllama est disponible
if (UAIChatPlus_Util::Cllama_IsValid())
{
UE_LOG(LogTemp, Display, TEXT("Cllama is available"));
}
// Vérification de la prise en charge GPU
if (UAIChatPlus_Util::Cllama_IsSupportGpu())
{
UE_LOG(LogTemp, Display, TEXT("GPU acceleration supported"));
}
// Obtenir les backends pris en charge
TArray<FString> Backends = UAIChatPlus_Util::Cllama_GetSupportBackends();
for (const FString& Backend : Backends)
{
UE_LOG(LogTemp, Display, TEXT("Backend: %s"), *Backend);
}
// Préparer les chemins des modèles dans Pak
FString ModelPath = TEXT("Models/my_model.gguf");
FString ActualPath = UAIChatPlus_Util::Cllama_PrepareModelPathFromPak(ModelPath);
// Si le modèle est dans un Pak, il sera décompressé dans un répertoire temporaire et le nouveau chemin sera renvoyé
}
Fonctions auxiliaires de fichiers
void UMyClass::FileUtilities()
{
// Obtenir le répertoire de base du plugin
FString PluginDir = UAIChatPlus_Util::GetPluginBaseDir(TEXT("AIChatPlus"));
UE_LOG(LogTemp, Display, TEXT("Plugin dir: %s"), *PluginDir);
// Copie de répertoire
FString SourceDir = FPaths::ProjectContentDir() / TEXT("Source");
FString DestDir = FPaths::ProjectSavedDir() / TEXT("Backup");
bool bCopied = UAIChatPlus_Util::CopierRepertoire(SourceDir, DestDir, true); // true = écraser
}
Modèle de Prompt
void UMyClass::PromptTemplates()
{
// Obtenir la liste des fichiers JSON de modèle Prompt
TArray<FDirectoryPath> ExtraDirs; // Répertoires de recherche supplémentaires (optionnels)
TArray<FFilePath> TemplateFiles = UAIChatPlus_Util::GetPromptTemplateJsonFiles(ExtraDirs);
// Chargement du modèle de Prompt
TArray<FAIChatPlus_PromptTemplate> Templates = UAIChatPlus_Util::GetPromptTemplates(TemplateFiles);
for (const FAIChatPlus_PromptTemplate& Template : Templates)
{
UE_LOG(LogTemp, Display, TEXT("Template: %s"), *Template.Name);
}
}
Original: https://wiki.disenone.site/fr
This post is protected by CC BY-NC-SA 4.0 agreement, should be reproduced with attribution.
Visitors. Total Visits. Page Visits.
Ce message a été traduit en utilisant ChatGPT, veuillez faire vos retours iciSignalez toute omission.