<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[The Breakfast Code ☕]]></title><description><![CDATA[AI Software Engineer | Technial Director | Python Allrounder

But first, coffee ☕]]></description><link>https://dev.alexdjulin.ovh</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1635943443991/Ney-29yh0.png</url><title>The Breakfast Code ☕</title><link>https://dev.alexdjulin.ovh</link></image><generator>RSS for Node</generator><lastBuildDate>Tue, 21 Apr 2026 07:42:08 GMT</lastBuildDate><atom:link href="https://dev.alexdjulin.ovh/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Training and deploying a chat-moderator using PyTorch and AWS SageMaker]]></title><description><![CDATA[GitHub Repo
💡 Please note: The chat messages shown are strictly for testing purposes and do not reflect my personal views or manner of speaking. As a French person, I value respectful dialogue. 😊
⚠️ Be careful that following this project may genera...]]></description><link>https://dev.alexdjulin.ovh/training-and-deploying-a-chat-moderator-using-pytorch-and-aws-sagemaker</link><guid isPermaLink="true">https://dev.alexdjulin.ovh/training-and-deploying-a-chat-moderator-using-pytorch-and-aws-sagemaker</guid><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><category><![CDATA[BERT]]></category><category><![CDATA[classification]]></category><category><![CDATA[chatgpt]]></category><category><![CDATA[chatbot]]></category><category><![CDATA[Deep Learning]]></category><category><![CDATA[sagemaker ]]></category><category><![CDATA[AWS]]></category><category><![CDATA[pythonanywhere]]></category><category><![CDATA[Cloud]]></category><dc:creator><![CDATA[Alexandre Donciu-Julin]]></dc:creator><pubDate>Fri, 14 Mar 2025 09:05:52 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1741941996108/e7abee12-0123-4379-a9ca-e0e8d6fe2954.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741939139317/0df3fbb1-12f0-4ac3-aa7b-ce58c8267cea.gif" alt class="image--center mx-auto" /></p>
<p><a target="_blank" href="https://github.com/alexdjulin/chat-moderator-sagemaker">GitHub Repo</a></p>
<p>💡 Please note: The chat messages shown are strictly for testing purposes and do not reflect my personal views or manner of speaking. As a French person, I value respectful dialogue. 😊</p>
<p>⚠️ Be careful that following this project may generate some <strong>costs</strong> when training and deploying a model on AWS SageMaker. See the <a class="post-section-overview" href="#cleanup-and-costs">Cleanup and Costs</a> section at the end for more info.</p>
<h2 id="heading-project-description">Project Description</h2>
<p>This project implements a content moderation system for a chat application. It leverages a <strong>BERT-based</strong> model for toxic comment classification (detecting categories such as toxic, obscene, insult, etc.) and uses OpenAI's <strong>gpt-4o</strong> for generating conversational responses. The model is trained and deployed on <a target="_blank" href="https://aws.amazon.com/sagemaker/">Amazon SageMaker</a>, while the chat interface is served using a Flask web app deployed on <a target="_blank" href="https://www.pythonanywhere.com/">PythonAnywhere</a>. The purpose of this project was to gain valuable knowledge in Cloud solutions.</p>
<h2 id="heading-environment-setup">Environment Setup</h2>
<p>Clone the repository, create a virtual environment and install dependencies as follows. There are multiple requirements files in the repo, use <strong>requirements-project.txt</strong>, which should cover everything you need.</p>
<pre><code class="lang-bash">https://github.com/alexdjulin/chat-moderator-sagemaker.git
<span class="hljs-built_in">cd</span> chat-moderator-sagemaker
python -m venv venv
<span class="hljs-built_in">source</span> venv/bin/activate  <span class="hljs-comment"># On Windows: venv\Scripts\activate</span>
pip install -r requirements-project.txt
</code></pre>
<p>This project has been developed on <strong>python 3.12</strong>. However, SageMaker uses <strong>python 3.8</strong>, and pythonanywhere <strong>python 3.10</strong>, that's why some modules are listed under older versions for compatibility reason in the different requirements files.</p>
<h2 id="heading-dataset-download-and-review">Dataset Download and Review</h2>
<p>I trained the model on the <a target="_blank" href="https://www.kaggle.com/c/jigsaw-toxic-comment-classification-challenge/data">Jigsaw Toxic Comment Classification Challenge</a> dataset, hosted on Kaggle. It consists of user-generated comments from <strong>Wikipedia discussions</strong>. Each comment is labeled for <strong>six</strong> types of toxicity:</p>
<ul>
<li><p>toxic</p>
</li>
<li><p>severe_toxic</p>
</li>
<li><p>obscene</p>
</li>
<li><p>threat</p>
</li>
<li><p>insult</p>
</li>
<li><p>identity_hate</p>
</li>
</ul>
<p>The dataset is structured as a <strong>CSV file</strong> where each row contains a comment and corresponding binary labels (1 for presence, 0 for absence of a toxicity type). It is meant to train models to classify whether a comment belongs to one or more of these categories. This dataset is widely used for <strong>natural language processing</strong> tasks, particularly for content moderation and <strong>text classification</strong>, helping to build models that detect harmful language in online discussions, which is exactly what we need.</p>
<h3 id="heading-download-the-dataset">Download the Dataset</h3>
<p>You can download the dataset from Kaggle directly or use the <strong>download_dataset.py</strong> script, that will download and unzip it in a <strong>data</strong> folder. You need Kaggle credentials for this, see <a target="_blank" href="https://www.kaggle.com/docs/api">Documentation</a>.</p>
<pre><code class="lang-plaintext">python download_dataset.py
</code></pre>
<p>On success, the script should print the shape, column names and comments distribution.</p>
<pre><code class="lang-plaintext">Dataset downloaded successfully. Shape: (159571, 8)

Column names: ['id', 'comment_text', 'toxic', 'severe_toxic', 'obscene', 'threat',
'insult', 'identity_hate']

Sample toxic comments distribution:
toxic            15294
severe_toxic      1595
obscene           8449
threat             478
insult            7877
identity_hate     1405
dtype: int64
</code></pre>
<h3 id="heading-review-the-dataset">Review the Dataset</h3>
<p>In the notebooks folder, go through the <strong>dataset_exploration</strong> notebook. It explores the content of the dataset, do a bit of data cleaning and feature engineering and generates two CSV files that can be used for training and testing: <strong>processsed_train.csv</strong> and <strong>processed_val.csv</strong>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741939329346/d321ca22-2c96-4005-9ab6-66048c82e954.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741939335873/4184e6e9-742a-4b34-93fc-c10e2975473c.png" alt class="image--center mx-auto" /></p>
<p>As you can see, the dataset is highly <strong>imbalanced</strong>, but I did not spend too much time fixing it, as this project was focused on cloud training and deployment.</p>
<h2 id="heading-create-model">Create Model</h2>
<p>The <strong>model</strong> folder contains 3 python scripts and 2 requirements files.</p>
<h3 id="heading-moderatorpy">moderator.py</h3>
<p>This module defines a <strong>BERT-based</strong> content moderation model for detecting toxic comments. It classifies text into <strong>six toxicity categories</strong> using a pre-trained <strong>BERT encoder</strong> and a <strong>classification head</strong>. The ContentModerator class provides a <strong>predict()</strong> method to return toxicity probabilities for given text. The <strong>forward()</strong> method processes tokenized input through BERT, extracts the <strong>CLS</strong> token representation, applies a linear classification layer, and <strong>outputs probability scores</strong> using a sigmoid activation function.</p>
<h3 id="heading-trainpy">train.py</h3>
<p>This script trains a <strong>BERT-based</strong> content moderation model on our dataset. It <strong>tokenizes</strong> text, prepares a PyTorch <strong>Dataset</strong> and <strong>DataLoader</strong>, and fine-tunes the <strong>ContentModerator</strong> model using Binary Cross-Entropy Loss (<strong>BCELoss</strong>). The model is trained on GPU (if available) and saves its weights to a <strong>.pth</strong> file. The <strong>train_model()</strong> function handles data loading, training, and logging, with <strong>TensorBoard</strong> support for tracking loss trends.</p>
<h3 id="heading-inferencepy">inference.py</h3>
<p>This script handles inference requests for the <strong>ContentModerator</strong> model in AWS <strong>SageMaker</strong>. It defines functions for loading the trained model (<strong>model_fn</strong>), parsing incoming requests (<strong>input_fn</strong>), running inference (<strong>predict_fn</strong>), and formatting responses (<strong>output_fn</strong>). The model is automatically loaded from /opt/ml/model/, and predictions are returned as <strong>JSON</strong>-formatted toxicity probabilities.</p>
<h3 id="heading-requirements-traintxt-requirements-infertxt"><strong>requirements-train.txt</strong>, <strong>requirements-infer.txt</strong></h3>
<p>They are packed into the <strong>model.tar.gz</strong> archive and sent to the <strong>S3 bucket</strong> respectively during the training and endpoint deployment steps. They list the dependencies specifically required for these steps. They are renamed automatically when used.</p>
<h2 id="heading-training-on-sagemaker">Training on SageMaker</h2>
<p>💡 While it would be easy to train a model locally or on Colab, the purpose of this project was to learn how to train and deploy a model using AWS SageMaker.</p>
<h3 id="heading-aws-setup">AWS Setup</h3>
<p>To train and deploy a job on SageMaker, you will need to do the following:</p>
<ul>
<li><p>Create an <a target="_blank" href="https://aws.amazon.com/free/">AWS account</a> (you can use the free tier to start)</p>
</li>
<li><p>Create an <strong>S3 bucket</strong> in your region. Mine is called <strong>chat-moderator</strong> and is stored in <strong>eu-central-1</strong>.</p>
</li>
<li><p>In your <strong>S3 bucket</strong>, create 3 folders: data, models and output. Upload <strong>processed_train.csv</strong> and <strong>processed_val.csv</strong> into the data folder.</p>
</li>
<li><p>In <strong>IAM</strong>, create a User with <strong>AdministratorAccess</strong>. You will need its Access Key and password to run inference on the model later (from PythonAnywhre)</p>
</li>
<li><p>In <strong>IAM</strong>, create a new Role with <strong>AmazonS3FullAccess</strong>, <strong>AmazonSageMakerFullAccess</strong> and <strong>CloudWatchFullAccess</strong> permissions. Copy it's ARN, you will need it for training and inference.</p>
</li>
<li><p>In <strong>Service Quotas</strong>, SageMaker AWS service, make sure you have at least one <strong>ml.g5.xlarge</strong> GPU for training and endpoint usage for your region. Request one if not.</p>
</li>
<li><p>Finally install the <a target="_blank" href="https://aws.amazon.com/cli/">AWS CLI</a> and call <strong>aws configure</strong> to enter your credentials.</p>
</li>
</ul>
<p>For more information, feel free to check <a target="_blank" href="https://github.com/Andreaswt/ai-video-sentiment-model">Andreas Trolle</a>'s in-depth tutorial on how to <a target="_blank" href="https://youtu.be/Myo5kizoSk0">Train and Deploy a Multimodal AI Model</a>.</p>
<h3 id="heading-create-training-job">Create Training Job</h3>
<p>Edit <strong>start_sagemaker_training.py</strong> with your bucket name and role ARN, then run it. It will go through the following steps and send a training job to SageMaker using the PyTorch estimator:</p>
<ul>
<li><p>Package <strong>train.py</strong>, <strong>moderator.py</strong> and <strong>requirements-train.txt</strong> into an archive</p>
</li>
<li><p>Upload it to a new folder in your <strong>S3 bucket</strong></p>
</li>
<li><p>Create the <strong>Training Job</strong> in SageMaker</p>
</li>
<li><p>Train and stop automatically once completed or if it runs into an error</p>
</li>
</ul>
<p>I trained my model for <strong>3 epochs</strong>, which took about <strong>2.5 hours</strong>.</p>
<pre><code class="lang-plaintext">python start_sagemaker_training.py
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741939581077/6f5f4748-83d2-47ef-8aa3-fa6a7cceac83.png" alt class="image--center mx-auto" /></p>
<p>The job should be visible in the SageMaker <strong>Training Jobs</strong> section. Click on it to access its logs in <strong>CloudWatch</strong> and make sure it is training as expected.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741939589514/8599185f-1de2-4dcb-8eb8-61f5e1652fb3.png" alt class="image--center mx-auto" /></p>
<p>⚠️ I was unable to start a training job simply by using the <strong>model.fit()</strong> method as shown in the <a target="_blank" href="https://sagemaker.readthedocs.io/en/stable/api/training/estimators.html#sagemaker.estimator.EstimatorBase.fit">documentation</a>. Therefore I had to implement the steps manually using the boto3 AWS SDK module.</p>
<h3 id="heading-download-trained-model">Download trained model</h3>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741939603693/3274e10f-1630-4444-92b9-b4ce6e3a5d91.png" alt class="image--center mx-auto" /></p>
<p>Once the training is done, you can access your model in your <strong>S3 bucket</strong>, in the <strong>output</strong> folder. Download and uncompress the <strong>model.tar.gz</strong> file. It should contain a <strong>model.pth</strong> model (the trained weights) and a <strong>tensorboard</strong> folder. Put them both in a <strong>model_output</strong> folder inside your local model folder.</p>
<p>If you don't want to go through the training steps, you can download <a target="_blank" href="https://alexdjulin.ovh/dev/chat_moderator_sagemaker/model.tar.gz">my model and tensorboard logs</a>.</p>
<p>You can preview the tensorboard metrics at <a target="_blank" href="http://localhost:6006/">this address</a> using the following command pointing to your tensorboard folder.</p>
<pre><code class="lang-plaintext">tensorboard --logdir=./model/model_output/tensorboard
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741939623610/d51d3032-02e6-4954-b60d-0e4b4ec1b4a7.png" alt class="image--center mx-auto" /></p>
<p>We can see the loss curve improving slightly over the <strong>3 epochs</strong>, although the improvement is very minimal.</p>
<h2 id="heading-creating-flask-app">Creating Flask App</h2>
<p>Now it's time to create our application.</p>
<h3 id="heading-frontend">Frontend</h3>
<p>For the frontend, I asked Claude 3.5 to create a simple <strong>chat interface</strong> for me. It generated the following 3 files:</p>
<ul>
<li><p><strong>index.html</strong> in the templates folder</p>
</li>
<li><p><strong>css/styles.css</strong> and <strong>js/main.js</strong> in the static folder.</p>
</li>
</ul>
<p>I asked for some modification to get a reactive chat, that would print all messages and then moderate them based on the model's predictions.</p>
<h3 id="heading-backend">Backend</h3>
<p>For the backend, I used Flask to run inference on the model. I hooked it up with a <strong>gpt-4o</strong> client to generate AI responses and offer a chat experience for testing purposes. I created an <strong>app_local.py</strong> file to run inference on my local model or on a sagemaker endpoint based on input arguments.</p>
<p>The flask endpoint goes through the following steps:</p>
<ul>
<li><p>Configure an OpenAI client using <strong>gpt-4o</strong> with a conversation context, so the AI assistant keeps track of the discussion</p>
</li>
<li><p>Load the local model stored in <strong>model_output</strong> and set it in <strong>eval</strong> model</p>
</li>
<li><p>Alternatively run inference on a <strong>SageMaker endpoint</strong>, to test the deployment done in the next steps</p>
</li>
<li><p>Run predictions on the model and classify the user's messages</p>
</li>
<li><p>Send the results to the frontend to display the chat messages with or without moderation, depending on the model's predictions</p>
</li>
</ul>
<p>You can run the Flask chat app like this and access it at <a target="_blank" href="http://127.0.0.1:5000">http://127.0.0.1:5000</a>.</p>
<pre><code class="lang-python">python app_local.py -m local  <span class="hljs-comment"># to infer on local model (default)</span>
python app_local.py -m sagemaker  <span class="hljs-comment"># to infer on sagemaker endpoint</span>
</code></pre>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741939804033/35d16211-3812-42f8-a433-30a6f00048f5.png" alt class="image--center mx-auto" /></p>
<p>As you can see, the second user message has been flagged as <strong>insulting</strong> and <strong>toxic</strong> and therefore moderated. For debugging purposes I am just crossing it out and adding any prediction results over the given <strong>threshold (0.5)</strong> in red below it.</p>
<p>We can use this local app to run tests on our model and re-train it if necessary to achieve better results.</p>
<h2 id="heading-deploying-model-on-sagemaker">Deploying Model on SageMaker</h2>
<p>Once we have a model we are happy about, we can deploy it as a <strong>SageMaker endpoint</strong> so we can infer on it from a local or deployed app.</p>
<pre><code class="lang-plaintext">python deploy_sagemaker_endpoint.py
</code></pre>
<p>Running the deployment script will go through the following steps:</p>
<ul>
<li>Wrap up the model into a <strong>model.tar.gz</strong> archive, along the required scripts to run inference on it inside a <strong>code</strong> folder:</li>
</ul>
<pre><code class="lang-plaintext">model.tar.gz  
├── model.pth  
└── code  
    ├── inference.py  
    ├── moderator.py  
    └── requirements.txt
</code></pre>
<ul>
<li><p>Upload the archive to the <strong>models</strong> folder of my <strong>S3 bucket</strong></p>
</li>
<li><p>Delete any existing SageMaker model or endpoint configuration</p>
</li>
<li><p>Create a <strong>SageMaker model</strong></p>
</li>
<li><p>Create a <strong>SageMaker endpoint configuration</strong></p>
</li>
<li><p>Deploy the endpoint</p>
</li>
</ul>
<p>⚠️ Same issue as for the training step, I was unable to use the <strong>model.deploy()</strong> method as indicated in the <a target="_blank" href="https://sagemaker.readthedocs.io/en/stable/api/inference/model.html">Documentation</a> to automate this step and I had to go through these steps manually using the boto3 module.</p>
<p>The endpoint should appear in <strong>SageMaker Inference Endpoints</strong> and be <strong>InService</strong> after a short time.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741939963336/add6e973-ae74-425a-a6c8-de9296b13f42.png" alt class="image--center mx-auto" /></p>
<p>You can use the following script to test the endpoint and infer on the model. You should get the prediction results corresponding to your input text. I set the <strong>threshold</strong> to <strong>0.5</strong> so it is pretty strict, for testing purposes. If any category has a prediction above this value, the message will be moderated.</p>
<pre><code class="lang-plaintext">python infer_on_sagemaker_endpoint.py
</code></pre>
<p>Alternatively you can test it in the local Flask app too by passing <strong>-m sagemaker</strong> as argument. You should get the same results as when infering on the local model.</p>
<pre><code class="lang-plaintext">python app_local.py -m sagemaker
</code></pre>
<h2 id="heading-deploying-flask-app-on-pythonanywhere">Deploying Flask App on PythonAnywhere</h2>
<p>This is our final step. Our model is now available in the AWS cloud, but our Flask app is still running locally. We need a simple online service where we can upload and run our app.</p>
<p>For simplicity and to avoid additional costs, I used <a target="_blank" href="https://www.pythonanywhere.com">pythonanywhere</a> which offers a free tier with <strong>512 MiB</strong> quota. It's not much but enough to deploy our frontend and backend scripts, as well as create a small environment to infer on the sagemaker endpoint.</p>
<p>Here are the steps I went through for this last step:</p>
<ul>
<li><p>Create a <strong>pythonanywhere</strong> free account</p>
</li>
<li><p>Create a new <strong>Web App</strong> using the manual setup (not Flask)</p>
</li>
<li><p>In the <strong>Files</strong> tab, upload the <strong>app_deployed.py</strong> and <strong>requirements-app.txt</strong> files, as well as the <strong>static</strong> and <strong>templates</strong> folders.</p>
</li>
<li><p>Uploaded an <strong>.env</strong> file too, where you specified your OPENAI_API_KEY, as well as the 3 AWS environment variables needed to run inference. See <strong>.env-template</strong>.</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741940111527/c111bd1c-17b4-4a3b-abff-7ec2be649876.png" alt class="image--center mx-auto" /></p>
</li>
<li><p>In the <strong>Consoles</strong> tab, open a bash console, create a virtual environment and install the dependencies from <strong>requirements-app.txt</strong>. See <a class="post-section-overview" href="#environment-setup">Environment Setup</a> section on how to do this.</p>
</li>
<li><p>In the <strong>Web</strong> tab, edit the WSGI configuration file as follows to point to your Flask app:</p>
</li>
</ul>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> sys

<span class="hljs-comment"># Add your project folder to the Python path</span>
project_home = <span class="hljs-string">'/home/alexdjulin/chat_moderator'</span>
<span class="hljs-keyword">if</span> project_home <span class="hljs-keyword">not</span> <span class="hljs-keyword">in</span> sys.path:
    sys.path.append(project_home)

<span class="hljs-comment"># Import Flask app</span>
<span class="hljs-keyword">from</span> app_deployed <span class="hljs-keyword">import</span> app <span class="hljs-keyword">as</span> application
</code></pre>
<ul>
<li><p>In <strong>Virtualenv</strong>, specify the path to the virtual environment you just created</p>
</li>
<li><p>Force <strong>HTTPS</strong> and add a <strong>password</strong> to access the app if needed</p>
</li>
</ul>
<p>Reload the app, which should now be accessible at your account address, like mine:<br /><a target="_blank" href="https://alexdjulin.pythonanywhere.com/">https://alexdjulin.pythonanywhere.com/</a> (password protected).</p>
<p>You should see the chat interface and be able to interact with the AI Assistant. If infering on the SageMaker endpoint does not work, an <strong>[uncensored]</strong> tag will be added next to the user's messages.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741940463204/3dc5f0dd-49db-48fd-a572-1cb43f99d942.png" alt class="image--center mx-auto" /></p>
<p>If you encounter any issue, check the Error log in the <strong>Web</strong> tab.</p>
<h2 id="heading-cleanup-and-costs">Cleanup and costs</h2>
<p>Be careful of the resulting costs when using cloud services!</p>
<p><strong>SageMaker training</strong> fees apply during the training step only, so you won't be charged once the job has completed or failed. Overall my latest model trained for <strong>2.5 hours</strong> (3 epochs) and cost me <strong>$5</strong>.</p>
<p>Make sure you delete your <strong>SageMaker endpoint</strong> when you don't need it anymore, as deployment costs about <strong>$1.5</strong> per hour of uptime.</p>
<p><strong>S3 bucket</strong> storage is very cheap, but feel free to delete your bucket once you are done working on the project and you don’t need it anymore. It’s easy to create a new one when required.</p>
<p><strong>IAM users</strong> and <strong>roles</strong> as well as <strong>GPU quota requests</strong> are free.</p>
<p>Hosting your app on <strong>pythonanywhere</strong> is free when using a Beginner account. Your are limited to <strong>512 MiB</strong> storage and your app is running for <strong>3 months</strong>. You will need to reload it after this time to keep it available.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>This project demonstrates how to build a <strong>scalable</strong> and <strong>efficient</strong> chat moderation system by integrating a <strong>BERT-based</strong> model trained on a public dataset for toxicity detection.</p>
<p>By leveraging Amazon <strong>SageMaker</strong>, the computationally intensive training and inference tasks are offloaded to <strong>AWS GPUs</strong>, ensuring <strong>optimized performance</strong>. Meanwhile, deploying the Flask app on <strong>PythonAnywhere</strong> provides a lightweight and accessible web interface, making the system easy to interact with.</p>
<p>This architecture effectively <strong>scales</strong> while maintaining a <strong>clear separation</strong> between content moderation and conversational AI.</p>
<p>A more streamlined approach could involve <strong>fully relying on an LLM</strong> to both generate responses and moderate user input simultaneously. This would likely <strong>improve classification accuracy</strong> and require <strong>minimal</strong> or even <strong>zero-shot learning</strong>. However, the current approach offers a <strong>local model</strong> inference option, which can be advantageous for <strong>privacy-focused applications</strong> where external API calls might not be desirable.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1741942068593/53822b67-d10f-4741-aeaf-b66871638af1.webp" alt class="image--center mx-auto" /></p>
]]></content:encoded></item><item><title><![CDATA[Mistral 7B - InDepth Paper Presentation]]></title><description><![CDATA[I had the opportunity to dive into the Mistral 7B paper and present it recently for a job interview. This is a recap of my presentation, covering the following topics:

🌟 Model Overview: Release context and promises

🧠 Architecture: Key design choi...]]></description><link>https://dev.alexdjulin.ovh/mistral-7b-indepth-paper-presentation</link><guid isPermaLink="true">https://dev.alexdjulin.ovh/mistral-7b-indepth-paper-presentation</guid><category><![CDATA[Python]]></category><category><![CDATA[llm]]></category><category><![CDATA[MistralAI]]></category><category><![CDATA[nlp]]></category><category><![CDATA[AI]]></category><category><![CDATA[generative ai]]></category><dc:creator><![CDATA[Alexandre Donciu-Julin]]></dc:creator><pubDate>Fri, 20 Dec 2024 11:21:46 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1734632782702/d09a577c-9142-43f7-b3af-b67fa9913391.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>I had the opportunity to dive into the <a target="_blank" href="https://arxiv.org/abs/2310.06825">Mistral 7B</a> paper and present it recently for a job interview. This is a recap of my presentation, covering the following topics:</p>
<ul>
<li><p>🌟 Model Overview: Release context and promises</p>
</li>
<li><p>🧠 Architecture: Key design choices for performance</p>
</li>
<li><p>📊 Benchmarks: Evaluation results, comparisons with peers</p>
</li>
<li><p>🔧 Fine-Tuning: Generalization across tasks and datasets</p>
</li>
<li><p>🚦 Guardrails: Strategies for ensuring responsible generation</p>
</li>
<li><p>💡 Key Use-Cases: Implementation scenarios</p>
</li>
<li><p>🏁 Conclusion: Key insights and discussion points</p>
</li>
</ul>
<p>What makes Mistral 7B so efficient, for such a small model? Buckle up and let's take a deep dive into its mechanics...</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/98C5Oho_YKE">https://youtu.be/98C5Oho_YKE</a></div>
<p> </p>
<h2 id="heading-arxiv-papers">Arxiv papers:</h2>
<p><a target="_blank" href="https://arxiv.org/abs/2310.06825">Mistral 7B</a> | <a target="_blank" href="https://arxiv.org/abs/1706.03762">Attention Is All You Need</a> | <a target="_blank" href="https://arxiv.org/abs/2004.05150">Longformer</a> | <a target="_blank" href="https://arxiv.org/abs/2305.13245">GQA</a></p>
]]></content:encoded></item><item><title><![CDATA[Bringing Alice to Life in 7 Days]]></title><description><![CDATA[Github repo
This is an in-depth presentation of my final project at Ironhack AI Engineering Bootcamp, November 2024.
https://www.youtube.com/watch?v=VuDmY0BEN10
 
Meet Alice, your personal AI Librarian! Alice isn’t just any chatbot — she’s a next-lev...]]></description><link>https://dev.alexdjulin.ovh/bringing-alice-to-life-in-7-days</link><guid isPermaLink="true">https://dev.alexdjulin.ovh/bringing-alice-to-life-in-7-days</guid><category><![CDATA[langchain]]></category><category><![CDATA[#agent]]></category><category><![CDATA[#chatbots]]></category><category><![CDATA[llm]]></category><category><![CDATA[GPT 4]]></category><category><![CDATA[RAG ]]></category><category><![CDATA[AI]]></category><category><![CDATA[nlp]]></category><category><![CDATA[generative ai]]></category><category><![CDATA[Python]]></category><dc:creator><![CDATA[Alexandre Donciu-Julin]]></dc:creator><pubDate>Wed, 18 Dec 2024 15:34:24 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1734535719705/6e05daae-b62b-40f3-96a9-a79493232ad2.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://github.com/alexdjulin/ik-multimodal-ai-librarian">Github repo</a></p>
<p>This is an in-depth presentation of my final project at <a target="_blank" href="https://www.ironhack.com/de-en/artificial-intelligence/remote">Ironhack AI Engineering Bootcamp</a>, November 2024.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=VuDmY0BEN10">https://www.youtube.com/watch?v=VuDmY0BEN10</a></div>
<p> </p>
<p>Meet Alice, your personal AI Librarian! Alice isn’t just any chatbot — she’s a next-level conversational assistant, leveraging the capabilities of an LLM and a flexible RAG, designed to:</p>
<ul>
<li><p>📚 Answer all your questions about books and literature.</p>
</li>
<li><p>🔍 Search a vector database for precise, relevant information.</p>
</li>
<li><p>🌐 Fetch detailed book insights straight from Wikipedia.</p>
</li>
<li><p>🎥 Pull transcripts of book reviews from YouTube.</p>
</li>
</ul>
<p>This architecture allows it to provide accurate and up-to-date information on various topics. The integration of a personality element, language models, and tools enables it to act as a virtual librarian with a rich user experience.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1734535790469/675bc1c3-3328-48fe-a1d2-ac7013fe2d96.png" alt class="image--center mx-auto" /></p>
<p>Alice was voted best bootcamp project and I presented her at the 2024 HackShow. Let me know what you think of the presentation and share insightful use-cases where you think Alice would shine! 🤖</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://www.youtube.com/watch?v=QKX2NF7ANj4&amp;t=0s">https://www.youtube.com/watch?v=QKX2NF7ANj4&amp;t=0s</a></div>
<p> </p>
<p>A huge thank you to Ironhack for giving me the opportunity to showcase my work, and once again to our amazing teachers <a target="_blank" href="https://www.linkedin.com/in/cfenollosa/">Carlos Fenollosa</a> and <a target="_blank" href="https://www.linkedin.com/in/isabella-frazeto/">Isabella Bicalho</a> Frazeto for their guidance and support throughout this journey.</p>
]]></content:encoded></item><item><title><![CDATA[Chatting with my movie-advisor using a langchain agent, RAG tools, Xata PostreSQL and TMDB]]></title><description><![CDATA[https://youtu.be/_nmiwgwRHxM
 
Github repo
This is a demo of my movie-advisor project: I created a chatbot driven by a langchain agent and RAG tools.
The agent is handling all conversations between me and the LLM. Based on my queries, it will search ...]]></description><link>https://dev.alexdjulin.ovh/chatting-with-my-movie-advisor-using-a-langchain-agent-rag-tools-xata-postresql-and-tmdb</link><guid isPermaLink="true">https://dev.alexdjulin.ovh/chatting-with-my-movie-advisor-using-a-langchain-agent-rag-tools-xata-postresql-and-tmdb</guid><category><![CDATA[AI]]></category><category><![CDATA[llm]]></category><category><![CDATA[GPT 4]]></category><category><![CDATA[RAG ]]></category><category><![CDATA[langchain]]></category><category><![CDATA[PostgreSQL]]></category><category><![CDATA[xata]]></category><dc:creator><![CDATA[Alexandre Donciu-Julin]]></dc:creator><pubDate>Sat, 10 Aug 2024 17:56:17 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1723313594359/eb58808e-b9bc-4451-b81a-56e7887ab969.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/_nmiwgwRHxM">https://youtu.be/_nmiwgwRHxM</a></div>
<p> </p>
<p><a target="_blank" href="https://github.com/alexdjulin/movie-advisor">Github repo</a></p>
<p>This is a demo of my movie-advisor project: I created a chatbot driven by a langchain agent and RAG tools.</p>
<p>The agent is handling all conversations between me and the LLM. Based on my queries, it will search for preferences and update my watch lists stored on the postgreSQL database Xata, or fetch missing information from the movie database TMDB.</p>
<p>I am using my other project <a target="_blank" href="https://github.com/alexdjulin/ai_chatbot">ai_chatbot</a> as submodule to initialise the agent and handle voice conversations using Google STT and Edge TTS engines.</p>
<p><strong>Tools:</strong> <a target="_blank" href="https://python.langchain.com/v0.1/docs/get_started/introduction/">Langchain</a> | <a target="_blank" href="https://www.compart.com/en/unicode/U+2B24">Xa</a><a target="_blank" href="https://xata.io/">ta</a> | <a target="_blank" href="https://www.themoviedb.org/">TMDB</a></p>
<p>Don't miss my other AI-related articles on my <a target="_blank" href="https://dev.alexdjulin.ovh/">dev blog</a>!</p>
<p>Music by Denys Kyshchuk from Pixabay.</p>
]]></content:encoded></item><item><title><![CDATA[Create a chatbot Intent Classifier based on fine-tuned GPT3]]></title><description><![CDATA[Intent classification is a pivotal process in natural language processing (NLP) where an AI system identifies the purpose or intention behind a user's input. This task involves analyzing a user's message and categorizing it into predefined intents, s...]]></description><link>https://dev.alexdjulin.ovh/intent-classifier-based-on-finetuned-gpt3</link><guid isPermaLink="true">https://dev.alexdjulin.ovh/intent-classifier-based-on-finetuned-gpt3</guid><category><![CDATA[Python]]></category><category><![CDATA[AI]]></category><category><![CDATA[openai]]></category><category><![CDATA[GPT 3]]></category><category><![CDATA[Beginner Developers]]></category><dc:creator><![CDATA[Alexandre Donciu-Julin]]></dc:creator><pubDate>Mon, 10 Jun 2024 12:53:33 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1718023878337/0bc6d0b5-7d27-45ca-8446-2fee236d7d89.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Intent classification is a pivotal process in natural language processing (NLP) where an AI system identifies the purpose or intention behind a user's input. This task involves analyzing a user's message and categorizing it into predefined intents, such as booking a flight, checking the weather, or requesting customer support. By understanding what the user wants to achieve, the chatbot can provide accurate and relevant responses.</p>
<h2 id="heading-project-presentation">Project Presentation</h2>
<p><a target="_blank" href="https://github.com/alexdjulin/openai-intent-classifier">GitHub repo</a></p>
<p>The goal of this project was to implement a zero- or few-shot intent classifier that can be used to provide inferencing services via an HTTP interface. For this, I used the <a target="_blank" href="https://www.kaggle.com/datasets/hassanamin/atis-airlinetravelinformationsystem">ATIS Dataset</a> to fine-tuned the latest version of OpenAI GPT3, in order to improve intent predictions.</p>
<p><img src="https://github.com/alexdjulin/openai-intent-classifier/raw/main/readme/classifier_demo.gif" alt class="image--center mx-auto" /></p>
<h2 id="heading-project-research">Project Research</h2>
<p>See the <a target="_blank" href="https://github.com/alexdjulin/openai-intent-classifier/blob/main/project_research.ipynb">project_research.ipynb</a> notebook on Github for my review of the ATIS dataset, as well as in-depth research on testing and fine-tuning GPT3 to perform intent classification.</p>
<p>The required modules to run the notebook code are listed at the beginning. Create a virtual environment if necessary and run the <strong>Modules Installation</strong> cell.</p>
<p>This notebook is available on <a target="_blank" href="https://www.kaggle.com/code/alexandredj/intent-classifier-based-on-fine-tuned-gpt3">Kaggle</a> too.</p>
<h2 id="heading-run-the-project">Run the Project</h2>
<p>See the <a target="_blank" href="https://github.com/alexdjulin/openai-intent-classifier">Readme</a> on Github to clone the project, install the required modules and run the Flask server. A Docker file to run the project in a container is available too.</p>
<p>Feel free to play around with the instructions in the <a target="_blank" href="https://github.com/alexdjulin/openai-intent-classifier/blob/main/prompt.jsonl">prompt</a> and <a target="_blank" href="https://github.com/alexdjulin/openai-intent-classifier/blob/main/intents.txt">intents</a> files to tweak the results to better match your use case.</p>
<p>You will need to fine-tune your own GPT3 model on the ATIS dataset, to get better results, as I cannot share my version. See the project research notebook for that.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>While intent classification has significantly advanced, challenges remain. Ambiguity in user messages, evolving language use, and domain-specific jargon can complicate accurate intent detection.</p>
<p>The future of intent classification in AI chatbots lies in continuous learning and adaptation. As models become more sophisticated, they will better understand and predict user intentions, making chatbots even more integral to digital communication.</p>
<p>In conclusion, intent classification is a cornerstone of modern AI chatbots, enabling them to understand and respond to user needs effectively. As technology advances, its role will only become more significant, driving innovation and enhancing the way we interact with machines.</p>
]]></content:encoded></item><item><title><![CDATA[Bicycle helmet detection using YOLOv8 and OpenCV]]></title><description><![CDATA[Combine and fine-tune YOLO models to detect if cyclists are wearing a helmet or not.
Github Project
I created and presented this project to complete my 120-hour Deep Learning bootcamp in April 2024. I then spent some more time improving it until I ge...]]></description><link>https://dev.alexdjulin.ovh/bicycle-helmet-detection-using-yolov8-and-opencv</link><guid isPermaLink="true">https://dev.alexdjulin.ovh/bicycle-helmet-detection-using-yolov8-and-opencv</guid><category><![CDATA[Python]]></category><category><![CDATA[YOLO]]></category><category><![CDATA[opencv]]></category><category><![CDATA[Beginner Developers]]></category><category><![CDATA[Deep Learning]]></category><category><![CDATA[Machine Learning]]></category><category><![CDATA[object detection ]]></category><dc:creator><![CDATA[Alexandre Donciu-Julin]]></dc:creator><pubDate>Mon, 06 May 2024 11:05:08 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1714993414785/60199d95-0a7a-4727-859a-562c70b663e2.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Combine and fine-tune YOLO models to detect if cyclists are wearing a helmet or not.</em></p>
<p><a target="_blank" href="https://github.com/alexdjulin/BikeHelmetDetection">Github Project</a></p>
<p>I created and presented this project to complete my 120-hour Deep Learning bootcamp in April 2024. I then spent some more time improving it until I get convincing results.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/WO02D-FHJfc">https://youtu.be/WO02D-FHJfc</a></div>
<p> </p>
<h1 id="heading-project-presentation">Project presentation</h1>
<p>Unlike other countries, Germany does not have a strict law regarding bicycle helmets. To this day, it is not compulsory to wear one, but only a recommended safety measure. Unfortunately, many people don't take this measure seriously, which can lead to severe injuries if an accident arises. Especially in a big city like Berlin, cyclist casualties hit the news on a daily bases.</p>
<p>My goal with this project was to raise cyclists awareness, using a friendly and direct feedback method. You may have seen those interactive street signs for cars: If you drive under the speed limit, you get a nice green smiley. But if you drive above it, you get an angry red one instead. Simple but effective. I surprised myself slowing down unconsciously, just for the pleasure to see a smiling face. Who doesn't need one on a Monday morning?</p>
<p>The goal of this project was therefore to train a Deep Learning model to detect on a video source if a cyclist is wearing a helmet or not. Displayed on a traffic light for instance, cyclists would get a direct feedback, in form of a green happy or red angry smiley.</p>
<p>Apart from raising awareness, the model could also be used to count cyclists and collect statistical data.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1714991717323/86f997f1-a2a9-417c-9426-c123afc82223.gif" alt class="image--center mx-auto" /></p>
<h1 id="heading-you-only-look-once">You Only Look Once</h1>
<p>The main reason why I chose this project was to finally get hold of a YOLO model and play around with it.</p>
<p><img src="https://raw.githubusercontent.com/ultralytics/assets/main/yolov8/banner-yolov8.png" alt="banner-yolov8.png" /></p>
<p>If you don't know about YOLO, check <a target="_blank" href="https://blog.roboflow.com/whats-new-in-yolov8/">this link</a> which goes in-depts about the internal structure and use-cases of the 8th version.</p>
<p>To sup-up, YOLO is an incredible Deep Learning model used to classify, detect, segment or track multiple elements of an image or video in real-time. Widely used everywhere in modern use-cases like healthcare, surveillance, self-driving cars or face detection, you can't set a foot in Computer Vision without hearing about it.</p>
<p><img src="https://raw.githubusercontent.com/ultralytics/assets/main/im/banner-tasks.png" alt="banner-tasks.png" /></p>
<p>I started browsing through the different versions that <a target="_blank" href="https://github.com/ultralytics/ultralytics">Ultralytics</a> offers and opted for YOLOv8n, which is the most lightweight version, hoping it would perform well on my old computer. Lightweight but still quite powerful, if you look at the numbers: 225 layers, 3.2M params for an input shape of 640 pixels.</p>
<p>You can chose between different versions, pre-trained on famous datasets:</p>
<ul>
<li><p>Coco (80 classes)</p>
</li>
<li><p>Open Image v7 (600 classes)</p>
</li>
<li><p>ImageNet (1000 classes)</p>
</li>
</ul>
<h1 id="heading-test-a-pre-trained-model">Test a pre-trained model</h1>
<p>You will find all my tests and method implementatoin in the <em>BikeHelmetDetection_Testing</em> notebook.</p>
<p>I started testing YOLOv8n pre-trained on <a target="_blank" href="https://storage.googleapis.com/openimages/web/index.html">Open Image v7</a>, as it can already detect a person (man, woman), a bicycle and a bicycle helmet.</p>
<p>I loaded the model and ran a prediction on a test image. Looking good!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1714991817965/4afa360e-7ea1-4c22-896f-be68d84ca87c.png" alt class="image--center mx-auto" /></p>
<p>When I tried it on a video file though, the results were not as good as I expected. While the model can classify a person or bicycle prettey accurately, it often fails at detecting the helmet, especially if it's not fully visible or only covers a small part of the picture.</p>
<p>On the other side, I tested the model version pre-trained on the COCO dataset and it seems to do a better job detecting a person and a bicycle. The issue is that there is no helmet class in the COCO dataset...</p>
<p>What if we fine-tune it and teach it how to recognize a helmet? Sounds like fun!</p>
<h1 id="heading-fine-tune-a-yolo-model">Fine-Tune a YOLO model</h1>
<p>My training steps are available in a <a target="_blank" href="https://www.kaggle.com/code/alexandredj/bikehelmetdetection-yolov8n-training">Kaggle</a> and <a target="_blank" href="https://colab.research.google.com/drive/1KGJ68orNqPCK3llccBD6_8MmcEXA1As3">Colab</a> notebooks.</p>
<p><a target="_blank" href="https://docs.ultralytics.com/modes/train/">Training YOLOv8n</a> is quite straightforward. The API is cristal clear and you can do that with 3 lines of code. But for those who like to get their hands dirty (like me), you can still access and play around with countless hyperparameters and callbacks.</p>
<p>But regardless how complex your model is, you still need a valuable dataset to train it if you want to get the best out of it.</p>
<p><img src="https://storage.googleapis.com/kaggle-datasets-images/687187/1204986/dac2fb8a09b4f44385f423e90c9900bf/dataset-cover.jpg" alt="dataset_cover" /></p>
<p>For this project I used the <a target="_blank" href="https://www.kaggle.com/datasets/andrewmvd/helmet-detection">Helmet Detection</a> dataset on Kaggle. It's not exactly what I need, as it's classifying all kind of driving helmets, also motorbike ones. But let's still give it a try.</p>
<ul>
<li><p>License: CC0: Public Domain</p>
</li>
<li><p>Content: 764 images and annotations</p>
</li>
<li><p>2 classes (with/without helmet)</p>
</li>
<li><p>Images: PNGs, various sizes</p>
</li>
<li><p>Annotations: XMLs, PASCAL VOC format</p>
</li>
</ul>
<h2 id="heading-convert-input-labels">Convert input labels</h2>
<p>The dataset provides PNG images and XML labels using the Pascal VOC format.</p>
<p>Example:</p>
<pre><code class="lang-xml"><span class="hljs-tag">&lt;<span class="hljs-name">annotation</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">folder</span>&gt;</span>images<span class="hljs-tag">&lt;/<span class="hljs-name">folder</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">filename</span>&gt;</span>BikesHelmets1.png<span class="hljs-tag">&lt;/<span class="hljs-name">filename</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">size</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">width</span>&gt;</span>400<span class="hljs-tag">&lt;/<span class="hljs-name">width</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">height</span>&gt;</span>300<span class="hljs-tag">&lt;/<span class="hljs-name">height</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">depth</span>&gt;</span>3<span class="hljs-tag">&lt;/<span class="hljs-name">depth</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">size</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">segmented</span>&gt;</span>0<span class="hljs-tag">&lt;/<span class="hljs-name">segmented</span>&gt;</span>
    <span class="hljs-tag">&lt;<span class="hljs-name">object</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">name</span>&gt;</span>With Helmet<span class="hljs-tag">&lt;/<span class="hljs-name">name</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">pose</span>&gt;</span>Unspecified<span class="hljs-tag">&lt;/<span class="hljs-name">pose</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">truncated</span>&gt;</span>0<span class="hljs-tag">&lt;/<span class="hljs-name">truncated</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">occluded</span>&gt;</span>0<span class="hljs-tag">&lt;/<span class="hljs-name">occluded</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">difficult</span>&gt;</span>0<span class="hljs-tag">&lt;/<span class="hljs-name">difficult</span>&gt;</span>
        <span class="hljs-tag">&lt;<span class="hljs-name">bndbox</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">xmin</span>&gt;</span>161<span class="hljs-tag">&lt;/<span class="hljs-name">xmin</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">ymin</span>&gt;</span>0<span class="hljs-tag">&lt;/<span class="hljs-name">ymin</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">xmax</span>&gt;</span>252<span class="hljs-tag">&lt;/<span class="hljs-name">xmax</span>&gt;</span>
            <span class="hljs-tag">&lt;<span class="hljs-name">ymax</span>&gt;</span>82<span class="hljs-tag">&lt;/<span class="hljs-name">ymax</span>&gt;</span>
        <span class="hljs-tag">&lt;/<span class="hljs-name">bndbox</span>&gt;</span>
    <span class="hljs-tag">&lt;/<span class="hljs-name">object</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">annotation</span>&gt;</span>
</code></pre>
<p>That's nice and easy to read, but Yolo requires a different label format as input. For each image, a txt file should list the classes and the bounding boxes top left and bottom right points, normalized btw 0 and 1.</p>
<p>{class} {bbox x1} {bbox y1} {bbox x2} {bbox y2}</p>
<pre><code class="lang-plaintext"># Example:
1 0.5162 0.1366 0.2275 0.2733
</code></pre>
<p>So I first generated these text labels from the xml files.</p>
<h2 id="heading-train-validation-split">Train / Validation Split</h2>
<p>I used the common 80/20 % ratio when dividing my PNG images and text labels into train and validation sets.</p>
<h2 id="heading-generate-the-training-config-file">Generate the training config file</h2>
<p>To initiate training, yolo requires a yaml file containing the paths to the train and validation sets, as well as a list of output classes. I generated it before training.</p>
<pre><code class="lang-yaml"><span class="hljs-attr">path:</span> <span class="hljs-string">dataset_dir</span>  <span class="hljs-comment"># root dir</span>
<span class="hljs-attr">train:</span> <span class="hljs-string">train/images</span>  <span class="hljs-comment"># train dir</span>
<span class="hljs-attr">val:</span> <span class="hljs-string">val/images</span>  <span class="hljs-comment"># val dir</span>

<span class="hljs-comment"># Classes</span>
<span class="hljs-attr">names:</span>
  <span class="hljs-attr">0:</span> <span class="hljs-string">without</span> <span class="hljs-string">helmet</span>
  <span class="hljs-attr">1:</span> <span class="hljs-string">with</span> <span class="hljs-string">helmet</span>
</code></pre>
<h2 id="heading-train-the-model">Train the model</h2>
<p>You need to chose a pre-trained model to start from, as you will get better results than training it from scratch.</p>
<p>I picked the version pre-trained on the COCO dataset.</p>
<p>As said, you can play around with hyperparameters and callbacks. I experimented a bit and eventually used the following settings:</p>
<ul>
<li><p>Start from pre-trained Coco model</p>
</li>
<li><p>No augmentation</p>
</li>
<li><p>150 epochs</p>
</li>
<li><p>Automatic Batch Size (16)</p>
</li>
<li><p>EarlyStopping with a patience of 50</p>
</li>
<li><p>Automatic Optimizer (AdamW)</p>
</li>
<li><p>Automatic Learning Rate (0.0017)</p>
</li>
<li><p>Dropout to reduce overfitting: 0.25</p>
</li>
</ul>
<p>I trained my model on Google Colab using a Tesla V100. It took 15 min for 150 epochs and cost me about 90 cts.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1714991858700/d0589b67-0719-4b4e-ac08-d245e86f67ae.jpeg" alt class="image--center mx-auto" /></p>
<h2 id="heading-evaluate-the-model">Evaluate the model</h2>
<p>Yolo outputs loads of graphs and metrics during the training, to help you visualize the results and assess your model's performance.</p>
<p>Let's have a look at the curves first:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1714991875042/f3349374-72c7-49e0-aa68-c90b4715d95b.jpeg" alt class="image--center mx-auto" /></p>
<p>The <strong>train/box</strong> loss and <strong>trail/cls</strong> loss shows us the model's accuracy when generating the bounding boxes (left) and classifying features as with or without helmet (right). Both decrease as expected over time. We can see that our model learns the classification part pretty quickly (drop over the first 20 epochs), but it takes longer to learn how to generate accurate bounding boxes (linear drop).</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1714991886244/b42ef23d-b6a8-4c83-b6a6-c2f91c8b1e63.jpeg" alt class="image--center mx-auto" /></p>
<p>The validation loss curves (testing the model on images it has never seen during training) give us similar drops for the classification. However, the bounding box loss seems to stagnate around 1.4 and does not decrease much, I'm not sure why.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1714991909432/62c1bdcc-f597-4ec9-9569-bda836fbd9b1.jpeg" alt class="image--center mx-auto" /></p>
<p>As for the other metrics like precision and recall, the curves go up and stabilize quickly.</p>
<p>The two bottom curves show the mean average precision at different IoU thresholds. The <a target="_blank" href="https://viso.ai/computer-vision/intersection-over-union-iou/">Intersection over Union</a> (IoU) threshold acts as a gatekeeper, classifying predicted bounding boxes as true positives if they pass the threshold and false positives if they fall below it. By adjusting the threshold, we can control the trade-off between precision and recall.</p>
<p>On the left curve, the threshold is set at 50%. Higher is generally better, leading to more accurate detections. On the right curve, the mAP ranges between 50 and 95% and gives a comprehensive view of the model's performance across different levels of detection difficulty.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1714991924202/95adbc00-5667-46a0-8480-5fcbb25ca313.png" alt class="image--center mx-auto" /></p>
<p>The Confusion Matrix is also a powerful graph to assess how accurate our model is at classifying heads with or without helmets, and quantifies false positives and false negatives. Our model seems to be doing a good job, predicting with accuracy if a person is wearing one or not (top left and middle squares). We have just a few false positives. It detects sometimes heads on empty backgrounds (right columnn), although this should not be too bothering for our use-case.</p>
<p>Note that our dataset seems to be uneven, with more features of people wearing a helmet than without (193 images Vs. 92 images here). This is confirmed on the labels graphs below. It is usually recommended to train a model on a dataset with an even distribution of images per classes.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1714991936926/1017c87b-4f4e-418a-aa68-de4db77dfefb.jpeg" alt class="image--center mx-auto" /></p>
<h2 id="heading-test-the-model">Test the model</h2>
<p>Now it's time to see what our model can do. Yolo will save the last and best version of it, which is not necessarely the same one. In case of EarlyStopping, you will get the best version over the amount of epochs defined as patience.</p>
<p>You will find my best version in the <strong>models</strong> folder.</p>
<p>Let's load it and predict on a few test pictures. I gathered all my tests in the <strong>BikeHelmetDetection_Testing</strong> notebook. A cell is running and comparing 3 predictions on the same image (COCO, OpenImageV7 and my fine-tuned model). Here are examples of how my model performs:</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1714991978837/4434a818-c610-454c-8f3a-662665902c45.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1714991992465/a254663e-72c4-49ee-8a16-2815a200d2ce.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1714992006678/f994b3be-b463-4180-b75b-edf6644d7d03.png" alt class="image--center mx-auto" /></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1714992017131/dbdbaca2-9b7a-4d49-ac6e-520d086deeb5.png" alt class="image--center mx-auto" /></p>
<p>Our model is doing a good job on images. But what about predicting on a video source, which is what we need in the end? Let's import OpenCV and give it a try on the webcam. Grab your helmet!</p>
<p><img src="https://github.com/alexdjulin/BikeHelmetDetection/assets/53292656/fa3e03c9-ebd9-448b-9bc1-d44c44cc26de" alt class="image--center mx-auto" /></p>
<p>All good. The confidence is not as high as I would have expected, but let's move on and see if this is enough for our use-case.</p>
<h1 id="heading-detecting-a-cyclist-wearing-a-helmet">Detecting a cyclist wearing a helmet</h1>
<p>We are now hitting the main challenge of this project:<br /><strong>How to detect a cyclist wearing a helmet?</strong></p>
<p>As discussed in the YOLO section above, Pre-trained versions of YOLOv8 can recognize up to 1000 classes out of the box (ImageNet). But you will not find a <em>'Cyclist with helmet'</em> class, rather separate classes like <em>'person'</em>, <em>'bike'</em> or <em>'helmet'</em>. We should therefore come up with an algorithm that not only will detect these classes but will be able to tell if this person is actually sitting on a bike and if this helmet is actually on the cyclist's head. And that's not as easy as it sound!</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1714992041988/b7d5e859-f7c8-4936-a78c-6375438e1f55.png" alt /></p>
<p><em>Are you making fun of me !?</em></p>
<p>In the process of fine-tuning YOLOv8n to classify heads with or without helmets, we altered the output layer of our model and we lost all its pre-trained proficiency. We picked the version pre-trained on COCO, which originally could classify persons and bikes. But now it can only classify heads with or without helmets, which is not enough for our use case. We don't want a random person walking in the street to get a red frowning smiley for not wearing a helmet!</p>
<p>Therefore, the most flexible solution would be to combine predictions from different models on the same input image. For instance:</p>
<ul>
<li><p>YOLOv8n pre-trained on COCO --&gt; Track Person and Bike</p>
</li>
<li><p>My fine-tuned version --&gt; Track Helmet</p>
<p>  <img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1714992079606/b7d432e2-6a17-4b3b-9261-699b502b81ea.png" alt class="image--center mx-auto" /></p>
</li>
</ul>
<p>We would end up with 2 or 3 bounding boxes and we can use simple maths to detect if a bounding box overlaps with another.</p>
<ul>
<li><p>Bike box inside a Person box --&gt; This is a cyclist</p>
</li>
<li><p>Helmet box inside a Person box --&gt; This person wears a helmet</p>
</li>
</ul>
<p>We can now combine both deductions and conclude if a cyclist is wearing a helmet or not.</p>
<p>Of course, this method will only work if a cyclist is clearly visible in the frame and detached from other background elements. If three of them are waiting closse to each others at a traffic light, bounding boxes may interfere and falsify the results. But our use-case aims at tracking and sending feedback to cyclists one at a time, so we should be fine.</p>
<h1 id="heading-building-our-final-solution">Building our final solution</h1>
<p>We now have all the cards we need in our hand. In the <strong>BikeHelmetDetection_Testing</strong> notebook, I defined some helper methods that helped me parse and process the prediction results:</p>
<ul>
<li><p><strong>parse_result</strong>: extracts all info we need from a YOLO prediction results, like classes, confidence values and bounding box coordinates.</p>
</li>
<li><p><strong>print_predictions</strong>: prints the parsed results.</p>
</li>
<li><p><strong>draw_result_on_frame</strong>: draws the predicted bounding box and class label on an input frame.</p>
</li>
<li><p><strong>get_bbox_center</strong>: returns the center of a bounding box.</p>
</li>
<li><p><strong>bbox_inside_bbox</strong>: checks if a bounding box overlaps with another one. For this, I simply check if the center of the child box is inside the parent box.</p>
</li>
<li><p><strong>combine_bboxes</strong>: combines predicted bounding boxes (person, bike, helmet) into one (cyclist with or without helmet).</p>
</li>
<li><p><strong>track_cyclist_with_helmet</strong>: this is our main method processing all the predictions, splitting them in classes (person, bike, helmet) and checking if what we see is a person on a bike and if this person is wearing a helmet or not.</p>
</li>
</ul>
<p>Thanks to these helpers, we can now build our OpenCV loop that will run through the video input, process each frame and draw the result on it before displaying it.</p>
<p>As said, we need a way to assign our predicted classes to different models. I'm doing this with a simple assignment dictionnary, where I list each model and the classes I expect it to predict. This will undoubtedly impact our solution's performance, as we run predictions on multiple models at once, for each single frame. To help with that, I am running predictions on different threads and I combine them at the end, before sending them to <strong>track_cyclist_with_helmet</strong>, to process and draw the result.</p>
<div class="embed-wrapper"><div class="embed-loading"><div class="loadingRow"></div><div class="loadingRow"></div></div><a class="embed-card" href="https://youtu.be/d6KX6_2A9qA">https://youtu.be/d6KX6_2A9qA</a></div>
<p> </p>
<p><em>Debug flag on: The bounding boxes of the different classes are displayed in white, the combined box in red or green.</em></p>
<p>Et voilà! Our cyclists now get a green or red bounding box around them and we can display the resulting smiley as feedback.</p>
<p>To finish, I moved everything to <strong>project_demo.py</strong> and added some more settings and features, like confidence thresholds to detect the different classes, an FPS counter and debugging elements to visualise predictions.</p>
<h1 id="heading-conclusion-and-improvements">Conclusion and Improvements</h1>
<p>This was intended as a short-term project. I spent two days on it to complete my module and another 4 days afterwards to come up with a clean solution. While I achieved my main objective, performance remains the main issue of this project. The tracking examples displayed on this page are played in real-time, but my prediction model is running at about 7 FPS on my local computer. This is not fast enough to detect cyclists accurately and give them a real-time feedback.</p>
<p>To move this project towards a final solution, we should first research what device could be used to detect cyclists in the street (Raspberry Pi?), what performance it offers and how we could optimize our prediction model to run much faster.</p>
<p>This project has been very instructive and was a perfect conclusion to my 4-week Deep Learning bootcamp (I got the best grade!). I hope I will get the chance to work on it further in the near future and eventually see it put to good use in the streets, hopefully raising cyclists awareness and reducing casualties.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1714992134606/59017a3b-ddbc-41ee-aad6-043031ff6e0e.png" alt /></p>
<h3 id="heading-be-safe-out-there-wear-a-helmet">BE SAFE OUT THERE, WEAR A HELMET !</h3>
]]></content:encoded></item><item><title><![CDATA[Training a CNN on Rock-Paper-Scissors images using Keras and OpenCV]]></title><description><![CDATA[My attempt at the Rock-Paper-Scissors classification problem.

Project Demo


Project Repo
This project is available on my github:
RockPaperScissorsCNN
Datasets
I used the following 3 datasets to train the CNN from scratch:
DRGFREEMAN - Edited to rem...]]></description><link>https://dev.alexdjulin.ovh/training-a-cnn-on-rock-paper-scissors-images-using-keras-and-opencv</link><guid isPermaLink="true">https://dev.alexdjulin.ovh/training-a-cnn-on-rock-paper-scissors-images-using-keras-and-opencv</guid><category><![CDATA[Python]]></category><category><![CDATA[Deep Learning]]></category><category><![CDATA[Beginner Developers]]></category><category><![CDATA[keras]]></category><category><![CDATA[opencv]]></category><dc:creator><![CDATA[Alexandre Donciu-Julin]]></dc:creator><pubDate>Wed, 06 Mar 2024 21:24:53 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1709760274845/1cdacf47-20db-4856-bf63-63b13e27f399.gif" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>My attempt at the Rock-Paper-Scissors classification problem.</em></p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1709743991924/aa469265-9473-4b75-b6c9-980f1ced64ac.png" alt /></p>
<h2 id="heading-project-demo">Project Demo</h2>
<iframe width="800" height="450" src="https://www.youtube.com/embed/mjglZWtWQCg?si=dNnfSfn36myYZfUK"></iframe>

<h2 id="heading-project-repo">Project Repo</h2>
<p>This project is available on my github:</p>
<p><a target="_blank" href="https://github.com/alexdjulin/RockPaperScissorsCNN">RockPaperScissorsCNN</a></p>
<h2 id="heading-datasets">Datasets</h2>
<p>I used the following 3 datasets to train the CNN from scratch:</p>
<p><a target="_blank" href="https://www.kaggle.com/datasets/drgfreeman/rockpaperscissors">DRGFREEMAN</a> - Edited to remove the green screen, see <em>remove_greenscreen</em> notebook<br /><a target="_blank" href="https://www.kaggle.com/datasets/sanikamal/rock-paper-scissors-dataset">SANI KAMAL</a> - Also available in tensorflow_datasets, see <em>download_dataset</em> notebook<br /><a target="_blank" href="https://www.kaggle.com/datasets/alexandredj/rock-paper-scissors-dataset">ALEXDJULIN</a> - I created this one myself</p>
<h2 id="heading-model">Model</h2>
<p>The latest version of my model is available here:<br /><a target="_blank" href="https://www.kaggle.com/models/alexandredj/rockpaperscissorscnn">rps_v01_56ep_0.9641acc_0.1089loss.h5</a></p>
<h2 id="heading-additional-ressources">Additional Ressources</h2>
<p>This model is inspired by the following tutorials and repos:<br /><a target="_blank" href="https://www.udemy.com/share/10143y3@ej9KzT_foe24Wk2_uXGAtEVenA9f0XWBnCTPnbixcYg1S9zeDuo0OOhYeRGyG5j-/">Python for Computer Vision with OpenCV and Deep Learning, by Jose Portilla</a><br /><a target="_blank" href="https://github.com/DrGFreeman/rps-cv">rps-cv repo from drgfreeman</a><br /><a target="_blank" href="https://medium.com/geekculture/rock-paper-scissors-image-classification-using-cnn-eefe4569b415">Rock-Paper-Scissors Image Classification Using CNN from Farah Amalia</a></p>
]]></content:encoded></item><item><title><![CDATA[Chatting with my digital clone in 4 languages, using python, audio2face and Unreal Engine]]></title><description><![CDATA[Don't like interviews, send your digital clone to do it :)


GitHub repository
This is the output of my AI avatar chat project, using Google speech recognition, OpenAI chat completion and Elevenlabs voice cloning. The lipsync is done using NVIDIA aud...]]></description><link>https://dev.alexdjulin.ovh/chatting-with-my-digital-clone</link><guid isPermaLink="true">https://dev.alexdjulin.ovh/chatting-with-my-digital-clone</guid><dc:creator><![CDATA[Alexandre Donciu-Julin]]></dc:creator><pubDate>Fri, 26 Jan 2024 11:17:58 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1706267424782/6d1d016e-262a-41f3-be92-4501d603d384.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Don't like interviews, send your digital clone to do it :)</em></p>
<iframe width="800" height="450" src="https://www.youtube.com/embed/WZQRwoXRtgA?si=kb7oRT85mfVD_dVE"></iframe>

<p><a target="_blank" href="https://github.com/alexdjulin/virtual-ai-avatar">GitHub repository</a></p>
<p>This is the output of my AI avatar chat project, using Google speech recognition, OpenAI chat completion and Elevenlabs voice cloning. The lipsync is done using NVIDIA audio2face and the avatar is a metahuman rendered in the Unreal Engine.</p>
<p>The code repo is covering the AI-chat interface only (yellow blocks), not the lipsync generation (in green) or the avatar creation and rendering (in red), which are done in 3rd-party software.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1708337752542/32f6fb4a-799e-46d6-ba0b-bfe2a5123353.png" alt class="image--center mx-auto" /></p>
]]></content:encoded></item><item><title><![CDATA[I asked ChatGPT to generate my Spotify playlists]]></title><description><![CDATA[Github Project
Project description
The goal of this simple project was to learn how to interact with the OpenAI API and get useful information from ChatGPT in a format I can easily manipulate (JSON dictionaries of artists and songs). On the other sid...]]></description><link>https://dev.alexdjulin.ovh/i-asked-chatgpt-to-generate-my-spotify-playlists</link><guid isPermaLink="true">https://dev.alexdjulin.ovh/i-asked-chatgpt-to-generate-my-spotify-playlists</guid><category><![CDATA[AI]]></category><category><![CDATA[openai]]></category><category><![CDATA[python beginner]]></category><category><![CDATA[spotipy]]></category><category><![CDATA[chatgpt]]></category><dc:creator><![CDATA[Alexandre Donciu-Julin]]></dc:creator><pubDate>Wed, 18 Oct 2023 07:06:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1697612626906/2fda3e4e-12e1-44d0-9b9b-f5a32e814405.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://github.com/alexdjulin/spotify-playlist-generator">Github Project</a></p>
<h2 id="heading-project-description">Project description</h2>
<p>The goal of this simple project was to learn how to interact with the <a target="_blank" href="https://github.com/openai/openai-python">OpenAI</a> API and get useful information from ChatGPT in a format I can easily manipulate (JSON dictionaries of artists and songs). On the other side, it uses the <a target="_blank" href="https://spotipy.readthedocs.io/en/2.22.1/">Spotipy</a> module to interact with Spotify and create playlists. Linked together, these two modules can help you create creative playlists from songs suggested by ChatGPT according to a given prompt (Example: "Peaceful songs to listen to when it's raining"). It's a great way to discover new tunes!</p>
<p>This project is built upon a tutorial by <a target="_blank" href="https://www.udemy.com/course/mastering-openai/">Colt Steele: Mastering OpenAI Python APIs</a> that I highly recommend, as it covers the basics of how ChatGPT works and how to interact with it. On top of the automatic playlist creation mode covered by the tutorial, I created an interactive mode, which lets you decide which songs should be added and gives you the possibility to blacklist artists and songs. I completed the project with additional methods to play and print playlists.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>To use this tool, you need both an OpenAI and a Spotify Developer account.</p>
<h3 id="heading-openai">OpenAI</h3>
<p>It's the company that created and offers access to different ChatGPT models. Go to the <a target="_blank" href="https://platform.openai.com/signup">OpenAI website</a> and create an account. It comes with some free credit to start and test the API. See <a target="_blank" href="https://platform.openai.com/docs/quickstart?context=python">documentation</a> to generate your own API-Key.</p>
<h3 id="heading-spotify">Spotify</h3>
<p>Create a <a target="_blank" href="https://www.spotify.com/us/signup">Spotify account</a> if you don't already have one, it's free. After that, log in to <a target="_blank" href="https://developer.spotify.com/">Spotify for Developers</a> and create a project. In the project settings, you can generate the client-ID and -Secret needed to connect and send requests to Spotify. See the <a target="_blank" href="https://developer.spotify.com/documentation/web-api/tutorials/getting-started">documentation</a> for more info.</p>
<p>Once you have your credentials to access both APIs, you can add them to <a target="_blank" href="https://github.com/alexdjulin/spotify-playlist-generator/blob/main/playlist_generator.py"><em>playlist_generator.py</em></a>. I am using the <a target="_blank" href="https://pypi.org/project/keyring/">keyring</a> module to store and retrieve mine, but you can use an .env file, store them in environment variables or just use them directly in the code. In this last case, be careful not to share your code with anyone!</p>
<pre><code class="lang-python"><span class="hljs-comment"># Set openai key</span>
openai.api_key = keyring.get_password(<span class="hljs-string">'openai_key'</span>, <span class="hljs-string">'main_key'</span>)
<span class="hljs-comment"># Set spotify client id and secret</span>
client_id=keyring.get_password(<span class="hljs-string">'spotify'</span>, <span class="hljs-string">'client_ID'</span>)
client_secret=keyring.get_password(<span class="hljs-string">'spotify'</span>, <span class="hljs-string">'client_secret'</span>)
</code></pre>
<h3 id="heading-spotify-desktop">Spotify Desktop</h3>
<p>If you want to be able to play songs during the playlist creation (in the interactive mode for instance), you will need <a target="_blank" href="https://www.spotify.com/us/download/">Spotify Desktop</a> installed on your machine. If you want to skip this part, comment all calls to the method <em>play_song_in_spotify</em> in <a target="_blank" href="https://github.com/alexdjulin/spotify-playlist-generator/blob/main/playlist_generator.py"><em>playlist_generator.py</em></a>, as it will not be able to ask your Spotify player to play the given songs.</p>
<h2 id="heading-installation">Installation</h2>
<p>Clone project and install required libraries (in a virtual environment for instance)</p>
<pre><code class="lang-python">git clone https://github.com/alexdjulin/spotify-playlist-generator.git
python -m venv venv
venv\Scripts\activate <span class="hljs-comment"># on windows</span>
source venv/bin/activate <span class="hljs-comment"># on macOS/linux</span>
pip install -r requirements.txt
</code></pre>
<h2 id="heading-how-to-use-it">How to use it</h2>
<p>The most convenient way to start the playlist generator is to call <a target="_blank" href="https://github.com/alexdjulin/spotify-playlist-generator/blob/main/main.py"><em>main.py</em></a>.</p>
<pre><code class="lang-plaintext">python main.py
</code></pre>
<p>It will ask for the 4 required input variables:</p>
<ul>
<li><p><strong>Playlist Prompt</strong> (-p):<br />  A short description sent to ChatGPT to generate the playlist. Example: <em>Best songs for a road trip with your best friend.</em></p>
</li>
<li><p><strong>Playlist Length</strong> (-l):<br />  How many songs should be in the playlist. Default value is 10.</p>
</li>
<li><p><strong>Playlist Name</strong> (-n):<br />  The playlist name on Spotify. Optional, leave blank to use the prompt as a name. Example: <em>Road Trip Songs</em>.</p>
</li>
<li><p><strong>Interactive Mode</strong> (-i):<br />  Y to activate or N to deactivate the interactive mode. When active, each song will be played in Spotify and the user can choose to Add the song, blacklist it or blacklist the artist.</p>
</li>
</ul>
<pre><code class="lang-plaintext">Playlist Prompt: Best songs for a road trip with your best friend
Playlist/Batch Length: 10
Playlist Name (leave blank to use prompt): Road Trip Songs
Interactive Mode? [y]es or [n]o: n
Creating playlist. Please wait...
&gt;&gt; end of playlist creation
</code></pre>
<p>Additionally, you can call <a target="_blank" href="https://github.com/alexdjulin/spotify-playlist-generator/blob/main/playlist_generator.py"><em>playlist_generator.py</em></a> and pass these arguments in the command line directly. Don't forget the quotes around the strings. The interactive mode flag is TRUE if you pass -i, FALSE if you don't pass it:</p>
<pre><code class="lang-bash"><span class="hljs-comment"># interactive flag off</span>
python playlist_generator.py -p <span class="hljs-string">"Best songs for a road trip with your best friend"</span> -l 10 -n <span class="hljs-string">"Road Trip Songs"</span>
<span class="hljs-comment"># interactive flag on</span>
python playlist_generator.py -p <span class="hljs-string">"Best songs for a road trip with your best friend"</span> -l 10 -n <span class="hljs-string">"Road Trip Songs"</span> -i
</code></pre>
<h2 id="heading-automatic-playlist-creation">Automatic Playlist Creation</h2>
<p>When the interactive mode is off, the playlist is created and filled automatically. It is great to create quick playlists, without worrying too much about the contents... Providing that you trust ChatGPT's suggestions, which can be sometimes surprising!</p>
<p>Output when the interactive mode is off:</p>
<pre><code class="lang-plaintext">----------------------------------------------------------------------------------------------------
------------------------------------ SPOTIFY PLAYLIST GENERATOR ------------------------------------
----------------------------------------------------------------------------------------------------
Prompt: Best songs for a road trip with your best friend
Length: 10
Name: Road Trip Songs
Interactive: False
----------------------------------------------------------------------------------------------------
1. Journey - Don't Stop Believin' | Escape (Bonus Track Version)
2. Smash Mouth - All Star | Astro Lounge
3. Bruce Springsteen - Born to Run | Born To Run
4. Guns N' Roses - Sweet Child O' Mine | Appetite For Destruction
5. John Denver - Take Me Home, Country Roads - Original Version | Poems, Prayers and Promises
6. ABBA - Dancing Queen | Arrival
7. Bon Jovi - Livin' On A Prayer | Slippery When Wet
8. Queen - Bohemian Rhapsody | Bohemian Rhapsody (The Original Soundtrack)
9. Lynyrd Skynyrd - Sweet Home Alabama | Second Helping (Expanded Edition)
10. Whitney Houston - I Wanna Dance with Somebody (Who Loves Me) | Whitney
----------------------------------------------------------------------------------------------------
</code></pre>
<p>Spotify should open automatically, the songs are added to the playlist and the first one from the list should play when done.</p>
<p><img src="https://github.com/alexdjulin/spotify-playlist-generator/raw/main/readme/road_trip_automatic.png" alt class="image--center mx-auto" /></p>
<h2 id="heading-interactive-playlist-creation">Interactive Playlist Creation</h2>
<p>If the interactive flag is on, we get a first batch of suggestions from ChatGPT (using the input playlist length as batch length) and we play the songs one by one in Spotify. For each song, we have the choice to:</p>
<ul>
<li><p>Add the current song to the Playlist</p>
</li>
<li><p>Blacklist this song (don't suggest it anymore)</p>
</li>
<li><p>Blacklist this artist (don't suggest any songs from this artist anymore)</p>
</li>
<li><p>Quit generating the playlist and keep it as it is</p>
</li>
</ul>
<pre><code class="lang-plaintext">Gary Jules - Mad World   (1/5)
[1] Add to Playlist
[2] Not this song
[3] Not this artist
[q] Quit playlist generation
Your choice:
</code></pre>
<p>When the choice is done, we jump to the other song. When the batch is done, we get the following choice:</p>
<ul>
<li><p>Ask ChatGPT for another batch of songs</p>
</li>
<li><p>Quit generating the playlist and keep it as it is</p>
</li>
</ul>
<pre><code class="lang-plaintext">Do you want another batch of songs?
[1] Yes, give me more songs!
[2] No, I'm down with this playlist
Your choice:
</code></pre>
<p>When asking ChatGPT for a new batch, we provide it with the list of songs we already have, as well as the lists of blacklisted songs and artists, to take into account for the next suggestions.</p>
<p>Ouput when the interactive playlist is done:</p>
<pre><code class="lang-plaintext">----------------------------------------------------------------------------------------------------
------------------------------------ SPOTIFY PLAYLIST GENERATOR ------------------------------------
----------------------------------------------------------------------------------------------------
Prompt: Lonely songs to play when it's raining
Length: 10
Name: Rainy Mood
Interactive: True
Artists Blacklist: {'The Carpenters', 'Willie Nelson'}
Songs Blacklist: {'Adore', "I Can't Make You Love Me"}
----------------------------------------------------------------------------------------------------
1. System Of A Down - Lonely Day | Hypnotize
2. a-ha - Crying in the Rain | East of the Sun, West of the Moon
3. Brook Benton - Rainy Night in Georgia | Brook Benton Today
4. Green Day - Boulevard of Broken Dreams | Boulevard of Broken Dreams
5. The Doors - Riders on the Storm | L.A. Woman
6. R.E.M. - Everybody Hurts | Automatic For The People
----------------------------------------------------------------------------------------------------
</code></pre>
<p><img src="https://github.com/alexdjulin/spotify-playlist-generator/raw/main/readme/rainy_mood_interactive.png" alt class="image--center mx-auto" /></p>
<p>See GitHub <a target="_blank" href="https://github.com/alexdjulin/spotify-playlist-generator/blob/main/README.md">readme</a> for additional notes and contributions.</p>
<h1 id="heading-conclusion">Conclusion</h1>
<p>This has been a fun little project and a good first experience playing around with the OpenAI API. The Spotipy library on the other side is very intuitive and you can quickly get around with it and interact with your Spotify account. Obviously, some prompts like "80's hits" will return you more accurate results than creative ones like "Sad songs to listen to when you just broke up and your fridge is empty". But I think switching the model to GPT4 and future models will probably improve on this.</p>
]]></content:encoded></item><item><title><![CDATA[My running map using python and folium]]></title><description><![CDATA[Click on the image to display the interactive one.
Link to the Github project
I'm a huge fan of maps, probably since I played around with my dad's roadmaps as a kid (he hated it, as he could never fold them back properly!). I cannot spend a day witho...]]></description><link>https://dev.alexdjulin.ovh/my-running-map-using-python-and-folium</link><guid isPermaLink="true">https://dev.alexdjulin.ovh/my-running-map-using-python-and-folium</guid><category><![CDATA[Python]]></category><category><![CDATA[Folium]]></category><category><![CDATA[maps]]></category><category><![CDATA[Beginner Developers]]></category><dc:creator><![CDATA[Alexandre Donciu-Julin]]></dc:creator><pubDate>Thu, 07 Sep 2023 21:00:47 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1694120745972/61f59301-bfd5-4e20-ba0f-39b526c7f12d.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><a target="_blank" href="https://alexdjulin.ovh/run/run_map/run_map.html"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8z6tud27udf1ajls1byq.png" alt="race_map" /></a></p>
<p><em>Click on the image to display the interactive one.</em></p>
<p><a target="_blank" href="https://github.com/alexdjulin/running-events-map">Link to the Github project</a></p>
<p>I'm a huge fan of maps, probably since I played around with my dad's roadmaps as a kid (he hated it, as he could never fold them back properly!). I cannot spend a day without fiddling with Google Maps. Although my trail running hobby and my GPS-watch addiction play a big part in that!</p>
<p>I have a <a target="_blank" href="https://run.alexdjulin.ovh/p/events.html">running blog</a> where I share reviews of events I took part in 🏃 But over the years, I kind of lost track of them. How many were there? Where did I run? How was the route? I need some kind of map to visualize them. What if I could make it so simple, that I would only need to update a Google spreadsheet with race information and the map on my blog would update accordingly? Let's see what we can do...</p>
<hr />
<p><a target="_blank" href="https://python-visualization.github.io/folium/latest/"><strong>FOLIUM</strong></a>. That's our code word for today. Folium is a Python module that is going to provide exactly what we need: Manipulate data in Python, then visualize it in a Leaflet map.</p>
<p>Let's start with a simple example: Creating a map from one race event logged in this <a target="_blank" href="https://docs.google.com/spreadsheets/d/1_yonZkHkeVzWXRvvxvmwGOxnbOGCzel9uX9MEFeAdeE">spreadsheet</a>.</p>
<p><a target="_blank" href="https://docs.google.com/spreadsheets/d/1WghWJbxdCeKpbi3-H_6FmBAv_jILD_1_woRhuKGJ190/edit?usp=sharing"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/229nw47uhsrv0k0xtr8j.png" alt="spreadsheet" /></a></p>
<p>See <a target="_blank" href="https://github.com/alexdjulin/running-events-map/blob/main/tutorial/tutorial.py">tutorial.py</a> on my git repo.</p>
<hr />
<h2 id="heading-download-the-spreadsheet">Download the spreadsheet</h2>
<p>Let's first download the Google spreadsheet as a CSV file, it will be easier to work with the data. I shared the document so anyone with the link can access it. The following command will download the spreadsheet.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> os
sheet_url = <span class="hljs-string">'https://docs.google.com/spreadsheets/d/1WghWJbxdCeKpbi3-H_6FmBAv_jILD_1_woRhuKGJ190/export?exportFormat=csv'</span>
current_folder = os.path.dirname(os.path.abspath(__file__))
csv_filepath = os.path.join(current_folder, <span class="hljs-string">'run_events.csv'</span>)
command = <span class="hljs-string">f'curl -L <span class="hljs-subst">{sheet_url}</span> -o <span class="hljs-subst">{csv_filepath}</span>'</span>
os.system(command)
</code></pre>
<p>You should now have a <em>run_events.csv</em> file next to your script.</p>
<hr />
<h2 id="heading-load-csv-data">Load CSV data</h2>
<p>Who doesn't love <a target="_blank" href="https://pandas.pydata.org/">Pandas</a> 🐼? Load the contents of the CSV file into a Python dictionary.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> pandas <span class="hljs-keyword">as</span> pd
data = pd.read_csv(csv_filepath).to_dict(orient=<span class="hljs-string">'records'</span>)[<span class="hljs-number">0</span>]
print(data)

<span class="hljs-comment"># Output:</span>
<span class="hljs-comment"># {'Date': '28.09.2014', 'Race': '41. Berlin Marathon', 'Latitude': 52.51625499, 'Longitude': 13.37757535, 'Time': '4:17:35', 'Link': 'https://www.bmw-berlin-marathon.com/'}</span>
</code></pre>
<hr />
<h2 id="heading-create-the-html-map">Create the HTML map</h2>
<p>Now we can start having fun! You will find Folium tutorials everywhere and the documentation itself is pretty straightforward. Let's generate a simple map and populate it with our data.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> folium

<span class="hljs-comment"># create map object and center it on our event</span>
<span class="hljs-keyword">import</span> folium
run_map = folium.Map(location=[data[<span class="hljs-string">'Latitude'</span>], data[<span class="hljs-string">'Longitude'</span>]], tiles=<span class="hljs-literal">None</span>, zoom_start=<span class="hljs-number">12</span>)

<span class="hljs-comment"># add Openstreetmap layer</span>
folium.TileLayer(<span class="hljs-string">'openstreetmap'</span>, name=<span class="hljs-string">'OpenStreet Map'</span>).add_to(run_map)

<span class="hljs-comment"># save and open map</span>
run_map.save(<span class="hljs-string">'run_map.html'</span>)
<span class="hljs-keyword">import</span> webbrowser
webbrowser.open(<span class="hljs-string">'run_map.html'</span>)
</code></pre>
<p>Run the code, the map should be generated, saved and opened in your default web browser. As you can see, it's empty and just centered on the race location (Berlin). Click images to see HTML maps.</p>
<p><a target="_blank" href="https://alexdjulin.ovh/dev/run_map/map_empty.html"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/w9a0vgarq1et17y5bx7d.png" alt="map_empty" /></a></p>
<hr />
<h2 id="heading-populate-the-map">Populate the map</h2>
<p>Let's add a marker point for our race to the map. We want it to be part of a 'Marathons' feature group, display the race name as a tooltip, assign a color to it and display a short legend in the corner.</p>
<pre><code class="lang-python"><span class="hljs-comment"># add feature group for Marathons</span>
fg_marathons = folium.FeatureGroup(name=<span class="hljs-string">'Marathons'</span>).add_to(run_map)

<span class="hljs-comment"># create marker and add it to marathon feature group</span>
folium_marker = folium.Marker(location=[data[<span class="hljs-string">'Latitude'</span>], data[<span class="hljs-string">'Longitude'</span>]], tooltip=data[<span class="hljs-string">'Race'</span>], icon=folium.Icon(color=<span class="hljs-string">'red'</span>))
folium_marker.add_to(fg_marathons)

<span class="hljs-comment"># add legend in top right corner</span>
run_map.add_child(folium.LayerControl(position=<span class="hljs-string">'topright'</span>, collapsed=<span class="hljs-literal">False</span>, autoZIndex=<span class="hljs-literal">True</span>))
</code></pre>
<p>Our marker now shows up at the given coordinates. It displays the name of the race if we hover over it and we can display or hide it from the corner legend.</p>
<p><a target="_blank" href="https://alexdjulin.ovh/dev/run_map/map_marker.html"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8yqk62zjtj8tfa1fond7.png" alt="map_marker" /></a></p>
<hr />
<h2 id="heading-add-pop-up-windows">Add pop-up windows</h2>
<p>It would be nice to make that marker clickable and offer more information about the race. Let's create an HTML iframe that will pop up when the user clicks on the marker. You will need some basic HTML knowledge for that, but nothing fancy at this point.</p>
<pre><code class="lang-python"><span class="hljs-comment"># create an iframe pop-up for the marker</span>
popup_html = <span class="hljs-string">f"&lt;b&gt;Date:&lt;/b&gt; <span class="hljs-subst">{data[<span class="hljs-string">'Date'</span>]}</span>&lt;br/&gt;"</span>
popup_html += <span class="hljs-string">f"&lt;b&gt;Race:&lt;/b&gt; <span class="hljs-subst">{data[<span class="hljs-string">'Race'</span>]}</span>&lt;br/&gt;"</span>
popup_html += <span class="hljs-string">f"&lt;b&gt;Time:&lt;/b&gt; <span class="hljs-subst">{data[<span class="hljs-string">'Time'</span>]}</span>&lt;br/&gt;"</span>
popup_html += <span class="hljs-string">'&lt;b&gt;&lt;a href="{}" target="_blank"&gt;Event Page&lt;/a&gt;&lt;/b&gt;'</span>.format(data[<span class="hljs-string">'Link'</span>])
popup_iframe = folium.IFrame(width=<span class="hljs-number">200</span>, height=<span class="hljs-number">110</span>, html=popup_html)

<span class="hljs-comment"># modify the marker object to display the pop-up</span>
folium_marker = folium.Marker(location=[data[<span class="hljs-string">'Latitude'</span>], data[<span class="hljs-string">'Longitude'</span>]], tooltip=data[<span class="hljs-string">'Race'</span>], popup=folium.Popup(popup_iframe), icon=folium.Icon(color=<span class="hljs-string">'red'</span>))
folium_marker.add_to(fg_marathons)
</code></pre>
<p>There it is. We even created a link on the URL, opening the event page in another tab. And since it's an HTML iframe, we can now basically display anything we want in it (pictures, videos, links, CSS styling, and so on).</p>
<p><a target="_blank" href="https://alexdjulin.ovh/dev/run_map/map_popup.html"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kfm0qe3aokx4cpl6kti3.png" alt="map_popup" /></a></p>
<hr />
<h2 id="heading-add-gpx-trace">Add GPX trace</h2>
<p>The cherry on top, it would be amazing to display the route, like you usually see on the event website.</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s80q7kkx6btlcva4sgv3.png" alt="map_gpx" /></p>
<p>This was easier than I thought. GPX files recorded by GPS watches or phones are XML documents easy to read and parse. I used the <a target="_blank" href="https://github.com/tkrajina/gpxpy">gpxpy</a> library for that, which is doing exactly what we need: read the GPX file and extract points/segments from it to display on our map. You can find GPX files everywhere, on <a target="_blank" href="https://www.strava.com/clubs/257389/group_events/680829">Strava</a> or <a target="_blank" href="https://www.komoot.com/tour/46519986">Komoot</a> for instance.</p>
<p>Here is how I opened and extracted segments from my own GPX file recorded during the race. I am using a <em>step</em> value when slicing all the points, to smooth out the curve (loading 1 every 10 coordinate points).</p>
<pre><code class="lang-python"><span class="hljs-comment"># parse gpx file</span>
<span class="hljs-keyword">import</span> gpxpy
gpx_file = <span class="hljs-string">'berlin_marathon_2014.gpx'</span>
gpx = gpxpy.parse(open(gpx_file))
track = gpx.tracks[<span class="hljs-number">0</span>]
segment = track.segments[<span class="hljs-number">0</span>]

<span class="hljs-comment"># load coordinate points</span>
points = []
<span class="hljs-keyword">for</span> track <span class="hljs-keyword">in</span> gpx.tracks:
    <span class="hljs-keyword">for</span> segment <span class="hljs-keyword">in</span> track.segments:
        step = <span class="hljs-number">10</span>
        <span class="hljs-keyword">for</span> point <span class="hljs-keyword">in</span> segment.points[::step]:
            points.append(tuple([point.latitude, point.longitude]))

<span class="hljs-comment"># add segments to the map</span>
folium_gpx = folium.PolyLine(points, color=<span class="hljs-string">'red'</span>, weight=<span class="hljs-number">5</span>, opacity=<span class="hljs-number">0.85</span>).add_to(run_map)

<span class="hljs-comment"># add the gpx trace to our marathon group</span>
folium_gpx.add_to(fg_marathons)
</code></pre>
<p>Wonderful, the trace is showing up as expected on the map. Notice that, as we added it to the Marathons feature group, it inherits the red color and is affected by the legend checkbox too. You can now play around with the segment's weight, opacity, color and step value to fine-tune it.</p>
<p><a target="_blank" href="https://alexdjulin.ovh/dev/run_map/map_gpx.html"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ttsolgc88jk6st65qpf3.png" alt="map_gpx" /></a></p>
<p>That's it, you have all you need to build a kick-ass map. Just add multiple lines to the spreadsheet and modify your data frame to load all the data into lists.</p>
<hr />
<h2 id="heading-improve-the-map">Improve the map</h2>
<p>If you browse through my <a target="_blank" href="https://github.com/alexdjulin/running-events-map/blob/main/run_map.py">run_map.py</a> script, you will notice that it is a bit more advanced. Here are some improvements I added to my map and to the project itself, to make it more appealing and fit my needs:</p>
<ul>
<li><p>Add additional tile layers (ArcGIS)</p>
</li>
<li><p>Group my events by type (halfs, marathons, ultras) and assign to each one a different color, also affecting the pop-up title and the G{X trace</p>
</li>
<li><p>Customize the pop-up window with a picture of the race, a nice font, links, etc</p>
</li>
<li><p>Move all paths and map settings to a JSON file, so there are no hard-coded values in the code and it is easier to change a setting (GPX trace weight or opacity for instance)</p>
</li>
<li><p>Put all race events info into the Google spreadsheet</p>
</li>
<li><p>Add an FTP upload method at the end to send the HTML, JPG and GPX files onto the online storage my blog is using</p>
</li>
</ul>
<p><a target="_blank" href="https://alexdjulin.ovh/run/run_map/run_map.html"><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8p46pf25dafs1gsrpbu2.png" alt="run_map_final" /></a></p>
<p>You can see the final result in the <a target="_blank" href="https://run.alexdjulin.ovh/p/events.html">Events</a> tab of my running blog.</p>
<p>Finally, you may notice that my script does not only update the map but also a table of events information displayed below it, as well as a little event-o-meter gadget in the sidebar. Both are generated when I run the script and are updated according to the updated Google spreadsheet information.</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0z9yc5bzydg8px25v6an.png" alt="event_table" /></p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zn5yams3l1cbigt2iusr.png" alt="eventometer" /></p>
<hr />
<h2 id="heading-conclusion">Conclusion</h2>
<p>I therefore succeeded in my holy quest to put all my running events on a pretty nice-looking map. All I need to do now, after completing a new event, is to fill in the information in the Google doc and provide a jpg thumbnail and the GPX trace of my run, then run the script to generate a new map and update the table and gadget. This last step could be automated of course, if we had our script running in the cloud and checking any updates done on the spreadsheet.</p>
<p>Have fun playing with folium and don't forget to share your maps! Take care and see you on the trail 🏔️🏃‍♂️</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1694119855752/b6ec4c3b-60f7-4869-b2e2-aed07839204e.gif" alt class="image--center mx-auto" /></p>
]]></content:encoded></item><item><title><![CDATA[Draw and email your Secret Santas using Python and SendGrid]]></title><description><![CDATA[Project on GitHub here.
It's that time of the year, AGAIN! It's been a fast one, but I do feel like I learned a lot, on my holy quest to become a rock-star programmer. That say, I would like to finish the year on a fun simple project. So when my mom ...]]></description><link>https://dev.alexdjulin.ovh/draw-and-email-your-secret-santas-using-python-and-sendgrid</link><guid isPermaLink="true">https://dev.alexdjulin.ovh/draw-and-email-your-secret-santas-using-python-and-sendgrid</guid><category><![CDATA[Python]]></category><category><![CDATA[Beginner Developers]]></category><category><![CDATA[python projects]]></category><category><![CDATA[Programming Blogs]]></category><dc:creator><![CDATA[Alexandre Donciu-Julin]]></dc:creator><pubDate>Tue, 14 Dec 2021 09:46:39 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1639474034972/r9QWpVJv9.gif" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b2yni5jhq1489qkrarad.gif" alt="Chandler Santa" />
Project on GitHub <a target="_blank" href="https://github.com/alexdjulin/secret-santa">here</a>.</p>
<p>It's that time of the year, AGAIN! It's been a fast one, but I do feel like I learned a lot, on my holy quest to become a rock-star programmer. That say, I would like to finish the year on a fun simple project. So when my mom asked me to draw our Secret Santas this year, I had it.</p>
<p>If you are not familiar with the concept, it's a nice way to celebrate xmas amongst a large group of friends or family members, withoug having to spend a salary on gifs. Every member of the group is assigned one recipient, and as his/her Secret Santa (since these random assignments are kept secret), you need to buy a gift for this person only. This way everyone will get a nice present instead of dozens of cheap ones.</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sfg479fbv54zv14s88fy.gif" alt="Toilet Seats cover" /></p>
<p>In the past years, I used a basic random name generator for that, but with the downside that I knew all the assignments and who my Secret Santa was. So this year, let's be clever and use a python script to do the draw and contact Santas per email.</p>
<h2 id="heading-get-list-of-santas-from-a-csv-file">Get list of Santas from a CSV file</h2>
<p>I leaned the Pandas library this year, which is great for extracting information from a CSV file. It's a user-friendly way to input data, easier than writing in the script directly. Here is our CSV file, with our six friends and the information we need to contact them.</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/17nrx833ug3uq56ckb7i.jpg" alt="CSV file" /></p>
<h2 id="heading-the-black-list">The Black List</h2>
<p><em>Black list, quid est?</em>
I like to spice things up and I thought it would be handy to add a black list feature, i.e. a list of names that one Santa should not get for any reason. In our case for instance:</p>
<ul>
<li>Ross and Rachel should not get each other, cause they had a big fight and they are 'on a break'</li>
<li>Monica and Chandler should not get each other, as they are married and they wanna give each others more expensive presents</li>
<li>Joey is broke, he bought a cheap present but it's for a guy so he cannot be one of the girl's Secret Santa</li>
<li>Phoebe likes everyone so her black list is empty</li>
</ul>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gqqeh4p88f3mmlq3mm9f.gif" alt="We were on a break" /></p>
<h2 id="heading-create-the-secretsanta-class">Create the SecretSanta class</h2>
<p>I learned programming using C++ and I have the CLASS word inprinted in caps inside my brain. So why not organise our script a bit by adding a <em>SecretSanta</em> class? Here are the class members and methods we need for the draw:</p>
<pre><code class="lang-python"><span class="hljs-class"><span class="hljs-keyword">class</span> <span class="hljs-title">SecretSanta</span>:</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__init__</span>(<span class="hljs-params">self, name, email, black_list = None</span>):</span>
        <span class="hljs-string">""" initialize class variables """</span>
        self.name = name
        self.email = email 
        self.recipient = <span class="hljs-literal">None</span>  <span class="hljs-comment"># will be allocated later</span>
        self.black_list = list()
        self.black_list.append(self.name)  <span class="hljs-comment"># adding own name</span>
        <span class="hljs-keyword">if</span> black_list:
            self.black_list += black_list.split(<span class="hljs-string">'|'</span>)

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">__repr__</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-string">""" override print method (optional) """</span>
        <span class="hljs-comment"># return string to print</span>

    <span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">contact_secret_santa</span>(<span class="hljs-params">self</span>):</span>
        <span class="hljs-string">""" contact recipient """</span>
        <span class="hljs-comment"># use SendGrid example here</span>
</code></pre>
<h2 id="heading-load-settings-from-a-json-file">Load settings from a json file</h2>
<p>Again, I find it more friendly to use external files to load variables instead of editing the code directly. So I usually add a <strong>settings.json</strong> file to all my projects. Here are the variables that we need to define there:</p>
<ul>
<li>Path to our CSV file containing Secret Santas information</li>
<li>Path to the email template (.txt or .html)</li>
<li>Max attempts that should be done while trying to assign recipients to secret santas. Due to the black lists, an assignment may not be possible and therefore this variable will break the while loop and raise an error.</li>
<li>Email of your SendGrid account</li>
<li>Personal SendGrid API key (private, don't share it with ANYONE)</li>
</ul>
<pre><code class="lang-json">{
    <span class="hljs-attr">"csv_file"</span>: <span class="hljs-string">"data/secret_santas_list.csv"</span>,
    <span class="hljs-attr">"email_file"</span>: <span class="hljs-string">"data/email.html"</span>,
    <span class="hljs-attr">"attempts_limit"</span>: <span class="hljs-number">100</span>,
    <span class="hljs-attr">"sg_sender_email"</span>: <span class="hljs-string">"youremail@domain.com"</span>,
    <span class="hljs-attr">"sg_api_key"</span>: <span class="hljs-string">"your_api_key"</span>
}
</code></pre>
<p>The json library helps you import and store these parameters in a SETTINGS dictionnary, that you can use further in your script.</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> json
<span class="hljs-keyword">with</span> open(<span class="hljs-string">"data/settings.json"</span>, <span class="hljs-string">'r'</span>) <span class="hljs-keyword">as</span> json_file:    
    SETTINGS = json.load(json_file)
print(SETTINGS[<span class="hljs-string">'csv_file'</span>])
<span class="hljs-comment"># output: 'data/secret_santas_list.csv'</span>
</code></pre>
<h2 id="heading-main-create-santas">Main - Create Santas</h2>
<p>We now have everything we need to write our main procedure.</p>
<p>To extract the CSV infos into a Pandas dataframe and create our SecretSanta instances is a child game.</p>
<pre><code class="lang-python"><span class="hljs-keyword">from</span> pandas <span class="hljs-keyword">import</span> read_csv
df = read_csv(csv_file_path).fillna(<span class="hljs-string">''</span>)
secret_santas = []
<span class="hljs-keyword">for</span> i <span class="hljs-keyword">in</span> range(len(df)):
    new_name = str(df[<span class="hljs-string">'Name'</span>][i])
    new_email = str(df[<span class="hljs-string">'Email'</span>][i])
    new_black_list = str(df[<span class="hljs-string">'Black List'</span>][i])
    new_santa = SecretSanta(name=new_name, email=new_email, black_list=new_black_list)
    secret_santas.append(new_santa)
</code></pre>
<h2 id="heading-main-draw">Main - Draw</h2>
<p>Finally we can draw our Secret Santas!</p>
<p>Since we introduced the black-list option, we face the issue that a draw might not be possible due to too many constraints (for instance, some poor guy who would be on everyone's black list!). I'm sure we could come up with a clever algorithm that would analyse all black-lists first and determine if a draw is possible or not. But let's keep it simple and take the easy road of trying, until we succeed or decide it's not gonna happen!</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/92jxdslxv1bis1euc1o0.gif" alt="Superman and Santa" /></p>
<p>We will therefore wrap everything inside a while loop, that will break if the draw is successful (everyone has been assigned as Secret Santa) or after a maximum numner of attempts (defined in our settings file).</p>
<p>Here are the main steps I am following, check the <a target="_blank" href="https://github.com/alexdjulin/secret-santa">GitHub project</a> for the (well-commented!) code:</p>
<ol>
<li>Initiate the while loop with our two conditions (success or reaching max attempts)</li>
<li>Shuffle our list of names and delete any previous assignment</li>
<li>Go through our list of SecretSanta instances and look for a recipient, which is not on the black-list. If we find one, we assign it and we jump to the next SecretSanta. If not, the draw fails and we start a new one (the while loop restart, we are back at step 2)</li>
<li>If we assign a recipient to all Secret Santas, the draw is successful and the while loop will break. If it never happens, the loop will break once reaching the max attemps.</li>
<li>Process the result: A succesful draw will trigger contacting the Secret Santas, while a failed one will raise an error and ask the user to review the input parameters.</li>
</ol>
<h2 id="heading-e-mailing-santas">E-mailing Santas</h2>
<p>This is our last step. We are going to use <a target="_blank" href="https://sendgrid.com/">Twilio SendGrid</a> for this, which provides a cloud-based service that assists businesses with email delivery. This is a very easy step thanks to their crystal-clear tutorials. Here are the steps you need to follow to use SendGrid services.</p>
<p><a target="_blank" href="https://sendgrid.com/free/">Create a SendGrid account</a><br />
You need to register for an account first. The free offer allows you to send up to 100 emails a day, which is good enough for our little project.</p>
<p><a target="_blank" href="https://docs.sendgrid.com/ui/sending-email/sender-verification">Create a Single Sender Verification</a><br />
Your recipients will receive emails from a specific address. Terefore this e-mail needs to be existing and verified as your own.</p>
<p><a target="_blank" href="https://docs.sendgrid.com/ui/account-and-settings/api-keys">Create a personal API key</a><br />
This is an important step. The API key is personal and should never be shared. It will serve as a badass password (seriously, just look at it!) that will need to be passed everytime e-mails are sent from your account. For simplicity, you can add it to your environment variables.</p>
<p><a target="_blank" href="https://github.com/sendgrid/sendgrid-python">Install the SendGrid python library</a><br />
Finally you can install the python library. The documentation gives you some simple examples on how to send your first emails. I did not need to dig much further, those are great. This project uses the <em>"Without Mail Helper Class"</em> example, that I implemented in my <em>contact_secret_santa()</em> class method.</p>
<h2 id="heading-personalising-the-email-content">Personalising the email content</h2>
<p>This is an optional but nice little step. I found it more joyful and christmassy to write the email contents as from the hand of Santa himself, asking people to help him spread the joy. Sendgrid let you use a text or html format for the contents of the email. So why not draft an html file with some pictures, colors, a handwriting font, etc? You can also put tags like [NAME] and [RECIPIENT] that will be replaced with your class attributes to address the person directly. Put some CSS to make it look more personal and believable. I don't know much about web dev but feel free to check my html template.</p>
<p>Once all these steps are done and if your draw was successful, you can now call the <em>contact_secret_santa()</em> method of all your instances. They will be notified within a few minutes and receive an email based on your template. Hey, you got one too!</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/bqlkhuuialzgrlu73hfw.gif" alt="Email contents" /></p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>I hope this little project taught you something new, whether you are playing with CSV and json files, trying to send emails or simply improving your python skills. It was a fun last projet to wrap up this year, which has been for me rich in learning new stuff and hopefully a few more steps towards my goals.</p>
<p>I wish you all a wonderful Christmas. Have a great Secret Santa sharing time with the ones you love (and the ones from your black list too!). Take care and read you next year :)</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7k5wk122bp6kh7rlvu8p.gif" alt="merry xmas" /></p>
<p>Friends pictures © Warner Bros. Entertainment Inc<br />
Cover picture from <a target="_blank" href="https://www.rachelmiddleton.co.uk/">Rachel Middleton</a></p>
]]></content:encoded></item><item><title><![CDATA[Live Link Face to Unreal MetaHuman Retarget]]></title><description><![CDATA[You can find the project files on my Github repo.

About MetaHumans
I recently started playing around with the mind-blowing MetaHuman (MH) feature from Unreal Engine. Through the Quixel Bridge editor, you can create and customize a highly realistic a...]]></description><link>https://dev.alexdjulin.ovh/livelinkface-to-unreal-metahuman-retarget</link><guid isPermaLink="true">https://dev.alexdjulin.ovh/livelinkface-to-unreal-metahuman-retarget</guid><category><![CDATA[Python]]></category><category><![CDATA[iphone]]></category><category><![CDATA[animation]]></category><dc:creator><![CDATA[Alexandre Donciu-Julin]]></dc:creator><pubDate>Wed, 03 Nov 2021 08:00:21 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1695727795261/e1588167-cb2f-41cf-9c3f-1a66a604c8c4.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>You can find the project files on my <a target="_blank" href="https://github.com/alexdjulin/LiveLinkFace-CSV-Retarget-For-Motionbuilder">Github repo</a>.</p>
<p><img src="https://cdn.hashnode.com/res/hashnode/image/upload/v1695727822175/c95e4822-b4e2-4fb1-a712-ba7638c037d4.gif" alt class="image--center mx-auto" /></p>
<h2 id="heading-about-metahumans">About MetaHumans</h2>
<p>I recently started playing around with the mind-blowing MetaHuman <em>(MH)</em> feature from Unreal Engine. Through the <a target="_blank" href="https://quixel.com/">Quixel Bridge</a> editor, you can create and customize a highly realistic avatar in a few clicks, and send it to your Unreal project. Everything is fine-tuned to get the best of Unreal shaders and will leave you speechless 😍 I invite you to take a peek at it, plenty of resources out there.</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/enycqzafiil9b9r10xia.jpg" alt="MetaHuman Editor" /></p>
<h2 id="heading-about-live-link-face">About Live Link Face</h2>
<p>As a mocap director, my interest quickly shifted over the live-retarget capabilities, i.e. how to transfer an actor's facial performance onto a metahuman, in real-time. Epic offers an app for that, which takes advantage of the iPhone's TrueDepth camera: <a target="_blank" href="https://apps.apple.com/us/app/live-link-face/id1495370836">Live Link Face</a> <em>(LLF)</em>. It also takes a few click to link your phone to your Unreal project and start driving your MetaHuman's face. Again, it's working 'Epic-ly' fast and well!</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zdh428kxgllv8vggcq7a.jpg" alt="Live Link Face" /></p>
<p>But there is the snag... The app was clearly designed to work in real-time with Unreal, where you can use the Sequencer to record facial data along body mocap, blueprints functions and so on. However, if used as a stand-alone app, it's another story.</p>
<p>LLF saves for each take 4 files:</p>
<ul>
<li><p>A CSV file containing the animation</p>
</li>
<li><p>A video file of the performance</p>
</li>
<li><p>A json file containing some technical information</p>
</li>
<li><p>A jpg thumbnail</p>
<p>  <img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q4p3t33487eg2hvdnmin.jpg" alt="LLF Export" /></p>
</li>
</ul>
<p>No maya scene, no unreal asset, not even a simple fbx file that we could use to import and clean the motion in a 3rd-party-software like MotionBuilder. Just a CSV file filled with random blendshape names and millions of values. Like that zucchini-peeler you got from aunt Edna for xmas, it's a nice gesture but what the heck am I gonna do with that!? 🤔</p>
<p>A quick look into the <a target="_blank" href="https://docs.unrealengine.com/4.27/en-US/AnimatingObjects/SkeletalMeshAnimation/FacialRecordingiPhone/">documentation</a> confirmed the idea:</p>
<blockquote>
<p><em>This data file is not currently used by Unreal Engine or by the Live Link Face app. However, the raw data in this file may be useful for developers who want to build additional tools around the facial capture.</em></p>
</blockquote>
<p>So, I guess we are on our own now!</p>
<h2 id="heading-lets-dig-in">Let's dig in</h2>
<p>It's not the most convenient format, but if you take a good look at this CSV file, it's all there: For each timecode frame (first column), you get a value of all the shapes defining the facial expression at that frame. These shapes come from Apple's <a target="_blank" href="https://developer.apple.com/documentation/arkit">ARKit Framework</a>, which uses the iphone's TrueDepth camera to detect facial expressions and generate blendshape coefficients. All we have to do is extract those time/value pairs and store then in some handy container. This is where the <a target="_blank" href="https://pandas.pydata.org/">Pandas</a> library 🐼 and its dataframes are gonna do wonders!</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0vc1fw8q9m2y9ass0lgs.jpg" alt="CSV contents" /></p>
<h2 id="heading-now-what-should-i-do-with-all-this">Now what should I do with all this?</h2>
<p>I need to recopy the animation values onto a target model that Unreal can import and read. Let's have a look at the <a target="_blank" href="https://www.unrealengine.com/marketplace/en-US/product/metahumans">MetaHuman project</a>, that you can find on the UE Marketplace.</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/6hbkmyu11x3e3lkvpu8l.jpg" alt="MetaHuman project" /></p>
<p>Facial and body motions are handled separately. A blueprint is applied to a facial version of the skeleton, which contains only the root, spine, neck, head and numerous facial joints, as Unreal relies on joint-based motions.</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mt2fso6wij7g1a4m32hg.jpg" alt="Export MH skeleton" /></p>
<p>I can get this skeleton out of Unreal by creating an animation pose and exporting it as FBX. Now we can open it in motionbuilder and... <strong>HOLY MOLY GUACAMOLE</strong>, what's all this!? 😵 I'm not talking about the Hellraiser facial rig but about the root joint properties. It contains a huge amount of custom properties!</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sul5m6h6v9wq6tr0utnc.gif" alt="root properties" /></p>
<p>I run a quick line of code and count about 1100 entries:</p>
<ul>
<li><p>254 properties starting with <strong>CTRL_expressions_</strong>, which are clearly blendshapes, like <em>CTRL_expressions_jawOpen</em></p>
</li>
<li><p>757 properties starting with <strong>head_</strong>, which seem to be additional correctives (fixing for instance a collision between two blendshapes activated at the same time).</p>
</li>
<li><p>35 properties starting with <strong>cartilage_</strong>, <strong>eyeLeft_</strong>, <strong>eyeRight_</strong> or <strong>teeth_</strong>, which are probably more correctives?</p>
</li>
<li><p>Last but not least: 53 properties, whose name are identical to the ARKit shapes in the CSV files from LLF, like <em>EyeBlinkLeft</em> or <em>JawOpen</em>.</p>
</li>
</ul>
<p>We can therefore assume that these last 53 properties are correspond to the ARKit blendshapes from the app and should receive our key values during retargeting.</p>
<h2 id="heading-how-is-unreal-computing-animations">How is Unreal computing animations?</h2>
<p>Before jumping into our favourite code editor, let's see how Unreal is reading and applying an animation onto the MetaHuman. I open the <em>Face_AnimBP</em> blueprint from the project, which is the one handling live-retargeting facial animations from the LLF app onto the rig.</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ixtle4q4x2gov1310wfo.jpg" alt="blueprint" /></p>
<p>BINGO, there it is! Inside a <em>Live Link Face</em> box, I find what seems to be a mapping block. It's doing all the connections. For each ARKit blendshape used by LLF, it is mapping the corresponding <em>CTRL</em> expressions and <em>head</em> correctives on the metahuman, by specifying an influence value between 0 (no influence) and 1 (full influence). You can see below how <em>BrowOuterUpLeft</em> triggers the 3 expressions <em>browLateralL</em>, <em>browRaiseInL</em> and <em>browRaiseOuterL</em> with different weights, while having no effect on any other expression.</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/aplzvo14rh8545oqtyup.jpg" alt="mapping" /></p>
<p>To confirm, I create random keys on the different properties in MotionBuilder and export the takes as FBX files to Unreal. It's working like a charm!</p>
<ul>
<li><p>Keys on the ARKit custom properties are read and retarget, but only if the animation goes through the mapping block first, so the animation blueprint is mandatory.</p>
</li>
<li><p>However, if I put keys on the <em>CTRL</em> shapes and <em>head</em> correctives, I can apply my animation directy onto the MH, without the need of an animation blueprint.</p>
</li>
</ul>
<p>As you can see, we have two solutions taking shape here: The easy road where we simply transfer animation values from the CSV file to the ARKit blendshape properties, or the long road where we convert them into MH expression and corrective values. Trying both?</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/enbxs0kyyq903b7yxxjz.gif" alt="Challenge accepted" /></p>
<h2 id="heading-retargeting-on-arkit-blendshapes">Retargeting on ARKit blendshapes</h2>
<p>As said, this is the easy stroll in the park. Our CSV file and our root bone are sharing the same ARKit blendshape information. All we need to do is transfer the keys. Here are the main steps I am following:</p>
<ol>
<li><p>Use Pandas to read the CSV file and extract all data. I create for this a BlendShape class, which receives the ARKit shape name, as well as animation keys, as pairs of timecode/values.</p>
</li>
<li><p>For all BlendShape objects created, I search on the skeleton root for the corresponding property name and create keys for all timecode/value pairs.</p>
</li>
<li><p>The scene framerate is very important here and should match the app's one, or keys won't be created at the correct frames.</p>
</li>
</ol>
<p>Once done, adjust your timespan to frame the animation and have a look at the ARKit properties of the root. They should be animated.</p>
<p>Export your animation as FBX to Unreal and test it in the Face_AnimBP blueprint. Don't forget it needs to go through that mapping block so Unreal can convert ARKit blendshape values to MH property values, which will be then turned into joint information. Cool beans!</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/to0w35lj7j8b1jz9vujo.gif" alt="arkit shapes retarget" /></p>
<h2 id="heading-retargeting-on-metahuman-properties">Retargeting on MetaHuman properties</h2>
<p>Now let's get our hands dirty and explore that second solution! We want to be able to use a retarget file directly onto our MH character without going through all this mapping knick-knacks. But for that we need to find a way to recreate this mapping node in MotionBuilder. As a reminder, it is linking each ARKit shape to the corresponding MH <em>ctrl</em> and <em>head</em> properties with weighted values ranging from 0 to 1. So we first need to extract these values and weights from Unreal 🥵</p>
<p>After looking around for a while, I noticed that Unreal nodes can be exported as T3D ascii files, which contain all information on what the node is doing. I tried and I must admit that it wasn't love at first sight. Fortunately, it's only 13 lines long!</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2h9c18n446v2mv7fzadp.gif" alt="T3D mapping file" /></p>
<p>But let's not judge a book by its cover. Again, if you take a closer look, everything we need is here. Time to see if my regex ninja training has paid off. After playing around with three patterns, I manage to extract from the T3D file:</p>
<ul>
<li><p>The ARKit blendshapes names</p>
</li>
<li><p>The MH properties names</p>
</li>
<li><p>The mapping weights for each property I add to my BlendShape class (characterising an ARKit shape), the target MH properties and the mapping weights. Done! I now have all the information I need for retargeting.</p>
</li>
</ul>
<p>Back to MotionBuilder, I can run my magic loops again. This time we have to be a bit more careful though:</p>
<ol>
<li><p>Read the T3D file and create BlendShape instances for all ARKit shapes.</p>
</li>
<li><p>Use Pandas to read the CSV file and extract the animation keys for each ARKit shape. I fill up these timecode/value pairs into my BlendShape class instances.</p>
</li>
<li><p>MotionBuilder is not very fast at finding root properties, maybe due to the high amount. To avoid searching for the same ones multiple times, I loop through the MH properties first and then through the timecode. For each frame, I check which ARKit blendshapes triggers my property and get the animation value weighted by my mapping information. For instance, if the ARKit value is 0.8 and the mapping value for this property is 0.5, then my animation key will be <em>0.8x0.5=0.4</em>. If multiple ARKit shapes are triggering it, I take the max value corresponding to the highest influence.</p>
</li>
<li><p>Again, be careful to retarget at the same framerate you used when recording takes.</p>
</li>
</ol>
<p>I see keys on my root's properties! Let's export it as FBX to Unreal and assign it to my Face component. <strong>WUNDERBAR!</strong> Unreal is reading the animation and transfering it to the facial joints without having to go through that mapping node anymore.</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qcwplulr4rrkuoyulpar.gif" alt="Property retarget" /></p>
<h2 id="heading-wrapping-up">Wrapping up</h2>
<p>To finalise the script and make it more user-friendly, I added the following features:</p>
<ul>
<li><p>A batching option, in case you point to a folder containing multiple CSV files</p>
</li>
<li><p>The possibility to offset the starting timecode, to synchronise the animation file with other sources</p>
</li>
<li><p>A simple PySyde UI for the user to enter the required paths files</p>
</li>
<li><p>Some logs and exception handlings to make sure that our batch is performing properly</p>
</li>
</ul>
<p>We are done, good job everyone! 😉</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>It only took patience and observation, as well as some ancient Regex voodoo to transfer the animation values recorded by the Live Link Face app as CSV files onto our brand new MetaHuman in Unreal. While not perfect, the result is good enough to move the animation to a cleaning stage.</p>
<p>This is therefore a quick way to record facial animations from your actors, even if they cannot perform in real-time. The ability to process them at a later stage using an animation software like MotionBuilder and a fairly simple python script like this one makes of Live Link Face a cheap and reliable facial motion-capture solution for MetaHumans characters.</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yrndowqn4uizqslv26ig.gif" alt="wink" /></p>
<p>I hope this will help you write your own solution. Feel free to message me if you need any help or buy me a croissant and cappuccino next time you're in town 🥐☕ Cheers!</p>
]]></content:encoded></item><item><title><![CDATA[A python regex to validate roman numerals]]></title><description><![CDATA[Note: This is my first post, I hope you'll like it :)
I'm not gonna lie to you... I LOVE REGEX!
As a kid, I grew up playing adventure games full of puzzles and riddles. Looking for the solution was a personal quest, a treasure hunt. Finding it was so...]]></description><link>https://dev.alexdjulin.ovh/a-python-regex-to-validate-roman-numerals</link><guid isPermaLink="true">https://dev.alexdjulin.ovh/a-python-regex-to-validate-roman-numerals</guid><category><![CDATA[Python]]></category><category><![CDATA[Regex]]></category><category><![CDATA[Beginner Developers]]></category><dc:creator><![CDATA[Alexandre Donciu-Julin]]></dc:creator><pubDate>Fri, 08 Oct 2021 21:35:20 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/upload/v1633728634445/GSa3dffg6.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p><em>Note: This is my first post, I hope you'll like it :)</em></p>
<h3 id="im-not-gonna-lie-to-you-i-love-regex">I'm not gonna lie to you... I LOVE REGEX!</h3>
<p>As a kid, I grew up playing adventure games full of puzzles and riddles. Looking for the solution was a personal quest, a treasure hunt. Finding it was so exciting, but not as much as jumping on another riddle!</p>
<p>When I discovered regular expressions on my journey to become a (good) python programmer, I felt that same excitement. I was blown away by the countless possibilities these were offering. Deciphering one was like suddenly being able to read hieroglyph, writing one was like discovering I could speak a foreign language. Although I know they should be used with caution and in special cases only, I keep pushing myself to use them everywhere I can. </p>
<p>That's why, when <a target="_blank" href="https://www.codewars.com/kata/51b66044bce5799a7f000003/train/python">Codewars</a> challenged me to write a function to convert roman numerals from/to Arabic numbers, I could not resist writing a regex to help me solve that problem.</p>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4s41hungkq8yztw9nsj3.jpg" alt="Alt Text" /></p>
<h3 id="enough-chit-chat-lets-get-our-hands-dirty">Enough chit-chat, let's get our hands dirty.</h3>
<p>My first task was to validate if the user input was a valid roman numeral. To sum up, roman numerals consists of the following symbols:<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/cte88ws99u84t39vn7uy.png" alt="image" />Source: <a target="_blank" href="https://en.wikipedia.org/wiki/Roman_numerals">Wikipedia</a></p>
<p>It seems that the thousands unit [M] does not extend past [MMM], which means that the biggest roman number would be [MMMCMXCIX], or 3999. I'm not sure if numbers could go higher than that and why the limit, anyway for the sake of this problem I limited myself with numbers between 1 and 3999.</p>
<p>Now the trick is that the symbols placement is very important. If you don't put them in the right order, the resulting number would be invalid and unreadable. As listed in the table up there, numerals should start with thousands [M] 1000, followed by hundreds [D/C] 500/100, then dozens [L/X] 50/10, and finally units [V/1] 5/1.</p>
<p>BUT, that's not it! Numerals can only repeat 3 times like [CCC] 300 before switching to a combo of two numerals like [CD] 400. So you can still have a [C] 100 before an [M] 1000, like in [CM] 900 for instance.</p>
<p>Bit confusing, isn't it? 
<img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hyd4koybrd8kqlqrf8be.png" alt="image" /></p>
<h3 id="alright-lets-recap-our-conditions">Alright, let's recap our conditions:</h3>
<ul>
<li>Roman numbers are ranging from [I] 1 to [MMMCMXCIX] 3999</li>
<li>Numerals should follow a precise order: [M] 1000 / [D] 500 / [C] 100 / [L] 50 / [X] 10 / [V] 5 / [I] 1</li>
<li>A numeral cannot repeat more than 3 times, it then uses a pair</li>
<li>The following pairs are allowed: [CM] 900 / [CD] 400 / [XC] 90 / [XL] 40 / [IX] 9 / [IV] 4</li>
</ul>
<p>Do you start to see our REGEX showing up? :)</p>
<h3 id="lets-translate-this-into-code">Let's translate this into code.</h3>
<p>For this we are going to use a tag I find really helpful when writing regex is the verbose one (<em>re.VERBOSE</em> or <em>re.X</em>) It allows you to spread your pattern on multiple lines and be more readable. Let's try it!</p>
<pre><code class="lang-python"><span class="hljs-keyword">import</span> re

<span class="hljs-function"><span class="hljs-keyword">def</span> <span class="hljs-title">is_roman_number</span>(<span class="hljs-params">num</span>):</span>

    pattern = re.compile(<span class="hljs-string">r"""    
                                ^M{0,3}
                                (CM|CD|D?C{0,3})?
                                (XC|XL|L?X{0,3})?
                                (IX|IV|V?I{0,3})?$
            """</span>, re.VERBOSE)

    <span class="hljs-keyword">if</span> re.match(pattern, num):
        <span class="hljs-keyword">return</span> <span class="hljs-literal">True</span>

    <span class="hljs-keyword">return</span> <span class="hljs-literal">False</span>
</code></pre>
<p>Wow, that looks amazing already! Let's take a closer look at these 4 lines:</p>
<ul>
<li><strong>^M{0,3}</strong> = Between 0 and 3 [M] at the beginning [^] of the string</li>
<li><strong>(CM|CD|D?C{0,3})?</strong> = One pair [CM] or one pair [CD] or [D], followed by up to 3 [C]. Each element is optional [?], as well as the whole block [()?]</li>
<li><strong>(XC|XL|L?X{0,3})?</strong> = One pair [XC] or one pair [XL] or [L], followed by up to 3 [X]. Each element is optional [?], as well as the whole block [()?]</li>
<li><strong>(IX|IV|V?I{0,3})?$</strong> = One pair [IX] or one pair [IV] or [V], followed by up to 3 [I]. Each element is optional [?], as well as the whole block [()?], which should be at the end of the string [$]</li>
</ul>
<h3 id="lets-test-our-code">Let's test our code</h3>
<p>I'm using a simple fstring calling my function and comparing the string against our pattern to validate the numeral or not:</p>
<pre><code class="lang-python">
num_valid = <span class="hljs-string">'MMDCCLXXIII'</span>
num_invalid = <span class="hljs-string">'CCCMMVIIVV'</span>

print(<span class="hljs-string">f"<span class="hljs-subst">{num_valid}</span> is <span class="hljs-subst">{<span class="hljs-string">'not'</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> is_roman_number(num_valid) <span class="hljs-keyword">else</span> <span class="hljs-string">''</span>}</span>a roman number"</span>)
print(<span class="hljs-string">f"<span class="hljs-subst">{num_invalid}</span> is <span class="hljs-subst">{<span class="hljs-string">'not '</span> <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> is_roman_number(num_invalid) <span class="hljs-keyword">else</span> <span class="hljs-string">''</span>}</span>a roman number"</span>)

<span class="hljs-comment"># Output:</span>
<span class="hljs-comment"># MMDCCLXXIII is a roman number</span>
<span class="hljs-comment"># CCCMMVIIVV is not a roman number</span>
</code></pre>
<p>That wasn't so bad after all! Now look at this and tell me it's not the most beautiful thing you've seen in your life:</p>
<pre><code class="lang-python">^M{<span class="hljs-number">0</span>,<span class="hljs-number">3</span>}(CM|CD|D?C{<span class="hljs-number">0</span>,<span class="hljs-number">3</span>})?(XC|XL|L?X{<span class="hljs-number">0</span>,<span class="hljs-number">3</span>})?(IX|IV|V?I{<span class="hljs-number">0</span>,<span class="hljs-number">3</span>})?$<span class="hljs-string">'</span>
</code></pre>
<p><img src="https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ubznh8stkc79wcn5njdk.gif" alt="Alt Text" /></p>
<p>That's all folks! Let me know if you are interested by the second part of the challenge: converting roman numerals from/to Arabic numbers and I will share my solution.</p>
<p>Stay safe out there and read you soon :)</p>
]]></content:encoded></item></channel></rss>