Python is my Love Language

A Valentine’s day gift for Laura

Kyle Pastor
5 min readFeb 12, 2025

Hey,

This year for Valentine’s day instead of getting you some flowers that will die a few days later (I may have gotten you some at the last minute, but that is a problem for future Kyle) I have decided to express my love in the most natural way for me.

In a short-form technical document that merges Python coding and 3D printing, my other two loves.

Now, if you are reading this I hope you were able to access it via the NFC tag embedded in the Lovogram 3D print that I have hopefully given to you by now. Fingers crossed I followed through. As for this article, I wanted to chronicle my creation to show you it is indeed a labour of love. So without further ado, and in all likeliness you will skip over the most of it, here we go.

Step 1: Saying I Love You

To kick this off I had to get me and kids to record a sweet “I Love You” message and have it available online. You may listed to it here:

The next step was to take this recording and use python code to convert the .mp3 file to a spectrogram. A spectrogram is a 2 dimensional representation of a Fourier transform over time. You love Fourier transforms, I know, but settle down please. #LoveOnTheSpectrum

Part 2: Sound to Picture

What this means is that each column of data is a moment in time, and each row is a frequency. The darker the region (or higher the print) the more the frequency is binned into this section. You do that for each moment in time and get a 2d heatmap. Here is the actual spectrogram from our love message:

Woosh! That is super cool! Look at that! Love in visual form! Of course I know you want to see the code also!

import librosa
import librosa.display
import matplotlib.pyplot as plt
import numpy as np
import sys
import pandas as pd

def mp3_to_spectrogram(mp3_file, output_image='spectrogram.png', output_csv='spectrogram.csv'):
# Load the audio file
y, sr = librosa.load(mp3_file, sr=None)

# Compute the spectrogram
S = librosa.feature.melspectrogram(y=y, sr=sr, n_mels=60, fmax=8000)
S_db = librosa.power_to_db(S, ref=np.max)

# Normalize amplitude values to range [0, 1]
S_db_norm = 5*(S_db - S_db.min()) / (S_db.max() - S_db.min())

# Resample time dimension to 60 steps
# S_db_resampled = librosa.util.fix_length(S_db_norm, size=60, axis=1)

# # Get time and frequency bins
# times = np.linspace(0, len(y) / sr, num=60) # 60 evenly spaced time steps
# freqs = librosa.mel_frequencies(n_mels=60, fmax=8000)

# Get time and frequency bins
times = librosa.times_like(S_db, sr=sr)
freqs = librosa.mel_frequencies(n_mels=60, fmax=8000)

# Save spectrogram data as CSV with integer time and frequency values
data = []
for i, t in enumerate(times):
for j, f in enumerate(freqs):
# Round time and frequency to the nearest integer
t_int = int(round(t)) # Round time to nearest integer
f_int = int(round(f)) # Round frequency to nearest integer
data.append([i, j, S_db_norm[j, i]])

df = pd.DataFrame(data, columns=['Time', 'Frequency', 'Amplitude'])
df.to_csv(output_csv, index=False)
print(f"Spectrogram data saved as {output_csv}")

# Plot the spectrogram
plt.figure(figsize=(10, 4))
librosa.display.specshow(S_db_norm, sr=sr, x_axis='time', y_axis='mel')
plt.colorbar(format='%+2.0f')
plt.title('We Love You Mommy')
plt.xlabel('Time')
plt.ylabel('Frequency')
plt.tight_layout()

# Save the spectrogram as an image
plt.savefig(output_image)
plt.close()
print(f"Spectrogram saved as {output_image}")

# Example call
mp3_to_spectrogram('C:\\Bitbucket\\Lovogram\\weloveumommy.mp3')

But Kyle! Where do you find the time!

With love Laura. I find the time with love.

Part 3: Picture to 3D Model

A picture is just data. In this case it has an x and y location of the pixels and the colour intensity is the z (height). All I did in the above was make a csv that could be read in. In Fusion 360 (the modelling software) you can use a “Add-In” and write python code. Here it is:

"""This file acts as the main module for this script."""

import adsk.core
import adsk.fusion
import csv
import os
import traceback

# Initialize the global variables for the Application and UserInterface objects.
app = adsk.core.Application.get()
ui = app.userInterface
design = app.activeProduct
rootComp = design.rootComponent
sketches = rootComp.sketches
design.designType = adsk.fusion.DesignTypes.DirectDesignType

def run(_context: str):
"""This function is called by Fusion when the script is run."""

try:
# Your code goes here.
xyPlane = rootComp.xYConstructionPlane

extrudes = rootComp.features.extrudeFeatures

# Open and read the CSV file
with open('C:\\Bitbucket\\Lovogram\\spectrogram.csv', mode='r', newline='') as file:
csv_reader = csv.reader(file)
data = [row for row in csv_reader] # Convert CSV to a list of rows


#10-25
#25-40
for i,row in enumerate(data):
if int(row[0])<100:
continue
if int(row[0])>150:
return

sketch = sketches.add(xyPlane)
sketch.name = "TheGrid" # Set the name of the sketch
lines = sketch.sketchCurves.sketchLines
recLines = lines.addTwoPointRectangle(adsk.core.Point3D.create(int(row[0]), int(row[1]), 0), adsk.core.Point3D.create(int(row[0])+1, int(row[1])+1, 0))
# Now get the laest profile

profile = sketch.profiles[len(sketch.profiles)-1]
# Extrude the profile
extrusion_height = float(row[2])+1

extrude = extrudes.addSimple(profile, adsk.core.ValueInput.createByString(str(abs(extrusion_height))), adsk.fusion.FeatureOperations.NewBodyFeatureOperation)
# extrude.bodies[0].name = 'John'
# Get the whole set of bodies

sketch_name = "TheGrid" # Change to your sketch name

for sketch in rootComp.sketches:
if sketch.name == sketch_name:
sketch.deleteMe()
# ui.messageBox(f"{sketch_name} has been deleted.", "Success")
break


except: #pylint:disable=bare-except
# Write the error message to the TEXT COMMANDS window.
app.log(f'Failed:\n{traceback.format_exc()}')

To do this I actually had to split it into 3 sections because my computer is not good enough….

After the code finished it made something that looks like this!

X:30%, Y:30%, Z:100% scaling (for posterior)

Part 4: Making love come to life

Now I just printed it! And glued! And added an NFC to the back! and here we are.

Long story short… I love you :)

— — — —

Today is Valentines and you loved it. Here is a pic.

wow. amazing.

--

--

Kyle Pastor
Kyle Pastor

Written by Kyle Pastor

Analytics | Full Stack | Data Science | MS Physics + MS Quant Finance → http://kapastor.github.io/

No responses yet