Hi everyone!
Iβm excited to share a fully working Streamlit app I developed that allows you to bulk upload multiple IFC files into Speckle, organized by workspace, stream, and branch β all with a simple UI and no coding required for the end user.
Whether youβre an AEC professional or a BIM coordinator managing hundreds of models, this tool will save you hours and help your team focus on analysis, not manual uploads.
You will be able to upload hundred files at the same time and you will be able to bypass the actual speckle ifc limit of 150mb, uploading largest ifc models over 2gb!
What this app does
This Streamlit script allows you to:
Authenticate with your Speckle account using a personal token
Select the target workspace (deduced from stream names)
Choose the stream inside that workspace
Select the branch where you want to upload
Choose a local folder full of IFC files
Upload each IFC to Speckle, creating a commit for each file
It automatically:
- Parses the IFC geometry and properties
- Converts IFC elements into Speckle objects with geometry and metadata
- Sends the full structure to the selected branch
- Names the commit after the file
What you need to install
You can run this app in a clean Python 3.10+ environment.
1. Create a virtual environment (recommended):
bash
CopyEdit
python -m venv venv
source venv/bin/activate # or venv\Scripts\activate on Windows
2. Install the required packages:
bash
CopyEdit
pip install streamlit specklepy ifcopenshell
How to get your Speckle token
- Go to https://app.speckle.systems
- Login and select the correct workspace (top-left menu)
- Click your avatar > My Tokens
- Create a new token with read/write permissions
- Paste the token in the script (or UI, depending on your setup)
How to use the app
- Launch the app:
bash
CopyEdit
streamlit run ifc_uploader.py
- In the UI:
- Confirm the Speckle URL (
https://app.speckle.systems
) - The app uses the embedded token (or you can make it interactive)
- Select the workspace β stream β branch
- Enter the folder path containing
.ifc
files - Click Upload All
Thatβs it! The app will parse each IFC file, serialize it into Speckle format (geometry + data), and send it as a new commit to the chosen branch.
Why this tool is useful
Massively speeds up IFC onboarding for new projects or QA pipelines
Removes human error from repetitive manual uploads
Fully compatible with Speckle Workspaces and multi-branch workflows
Easily customizable for additional metadata, Excel checks, or QA rules
Speckle-native: uses
specklepy
and works with commits/branches/streams directly
Works on any OS with Python and Streamlit installed
Lightweight: no dependencies on Revit, Navisworks, or paid software
Real world impact
In a scenario where you need to upload 50+ IFC files to a structured branch system (e.g., 10 buildings Γ 5 disciplines), doing this manually would take several hours. With this script, itβs reduced to a few clicks and minutes β especially valuable for those with paid workspaces and large data pipelines.
Want to extend it?
This script can be easily enhanced to:
- Create streams or branches dynamically
- Read model metadata from Excel
- Display 3D preview of each model inside the Streamlit app
- Trigger downstream automation (e.g., validation, analysis)
Feedback?
Iβd love to hear your thoughts and suggestions!
Let me know if youβd like a version with advanced features or support for other file types.
**sorry all the comments in the python script file are in italian
Cheers from,
Oscar
import os
import ifcopenshell
import ifcopenshell.geom
import streamlit as st
from specklepy.api.client import SpeckleClient
from specklepy.transports.server import ServerTransport
from specklepy.api import operations
from specklepy.objects import Base
from specklepy.objects.geometry import Mesh
# === CONFIGURAZIONE FISSA ===
SPECKLE_SERVER_URL = "https://app.speckle.systems/"
SPECKLE_API_TOKEN = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
@st.cache_resource
def get_speckle_client():
client = SpeckleClient(host=SPECKLE_SERVER_URL)
client.authenticate_with_token(SPECKLE_API_TOKEN)
return client
def extract_geometry(ifc_element):
settings = ifcopenshell.geom.settings()
settings.set(settings.USE_WORLD_COORDS, True)
try:
shape = ifcopenshell.geom.create_shape(settings, ifc_element)
geometry = shape.geometry
vertices = geometry.verts
faces = geometry.faces
if not vertices or not faces:
return None
speckle_faces = []
for i in range(0, len(faces), 3):
speckle_faces.append(3)
speckle_faces.extend([faces[i], faces[i+1], faces[i+2]])
return Mesh(vertices=list(vertices), faces=speckle_faces, units="millimeters")
except:
return None
def extract_properties(ifc_element):
props = Base()
props["global_id"] = ifc_element.GlobalId
props["name"] = getattr(ifc_element, "Name", "Unnamed Element")
props["ifcType"] = ifc_element.is_a()
props["ObjectType"] = getattr(ifc_element, "ObjectType", "N/A")
props["Tag"] = getattr(ifc_element, "Tag", "N/A")
props["expressID"] = ifc_element.id()
psets = {}
if hasattr(ifc_element, "IsDefinedBy"):
for rel in ifc_element.IsDefinedBy:
if rel.is_a("IfcRelDefinesByProperties"):
prop_set = rel.RelatingPropertyDefinition
if prop_set.is_a("IfcPropertySet"):
set_name = prop_set.Name
psets[set_name] = {}
for prop in prop_set.HasProperties:
if hasattr(prop, 'NominalValue') and prop.NominalValue:
value = getattr(prop.NominalValue, 'wrappedValue', prop.NominalValue)
psets[set_name][prop.Name] = value
props["PropertySets"] = psets
return props
def process_ifc(file_path):
ifc_file = ifcopenshell.open(file_path)
schema = ifc_file.schema.lower()
st.info(f"π IFC Schema: {schema}")
root = Base()
root.name = "IFC Model"
root["collectionType"] = "model"
storeys = {}
projects = ifc_file.by_type("IfcProject")
project_base = None
if projects:
project_base = Base()
project_base.name = getattr(projects[0], "Name", "Unnamed Project")
project_base.speckle_type = "IFCProject"
project_base["elements"] = []
for element in ifc_file.by_type("IfcProduct"):
props = extract_properties(element)
obj = Base()
for k in props.get_dynamic_member_names():
obj[k] = props[k]
obj.speckle_type = props["ifcType"]
geometry = extract_geometry(element)
if geometry:
obj["displayValue"] = [geometry]
if hasattr(element, "ContainedInStructure"):
for rel in element.ContainedInStructure:
if rel.is_a("IfcRelContainedInSpatialStructure"):
relating = rel.RelatingStructure
if relating.is_a("IfcBuildingStorey"):
sid = relating.GlobalId
if sid not in storeys:
sb = Base()
sb.name = getattr(relating, "Name", "Unnamed Storey")
sb.speckle_type = "IFCBuildingStorey"
sb["elements"] = []
storeys[sid] = sb
storeys[sid]["elements"].append(obj)
if project_base:
project_base["elements"].extend(storeys.values())
root["elements"] = [project_base]
else:
root["elements"] = list(storeys.values())
return root
# === INTERFACCIA STREAMLIT ===
st.title("π IFC Uploader per Workspace Speckle")
client = get_speckle_client()
try:
streams = client.stream.list()
except Exception as e:
st.error(f"β Errore recuperando gli stream: {e}")
st.stop()
# Group by workspace prefix
workspace_dict = {}
for stream in streams:
if "/" in stream.name:
ws, name = stream.name.split("/", 1)
else:
ws, name = "Default", stream.name
if ws not in workspace_dict:
workspace_dict[ws] = []
workspace_dict[ws].append((name, stream.id))
selected_workspace = st.selectbox("π Workspace", list(workspace_dict.keys()))
stream_options = {name: sid for name, sid in workspace_dict[selected_workspace]}
selected_stream_name = st.selectbox("π Seleziona Stream", list(stream_options.keys()))
selected_stream_id = stream_options[selected_stream_name]
try:
branches = client.branch.list(selected_stream_id)
if not branches:
st.warning("β οΈ Nessun branch in questo stream.")
st.stop()
except Exception as e:
st.error(f"β Errore caricando i branch: {e}")
st.stop()
branch_names = [b.name for b in branches]
selected_branch = st.selectbox("πΏ Seleziona Branch", branch_names)
folder_path = st.text_input("π Percorso locale della cartella contenente i file IFC")
if st.button("π Carica tutti gli IFC"):
if not os.path.isdir(folder_path):
st.error("β Percorso non valido.")
else:
ifc_files = [f for f in os.listdir(folder_path) if f.lower().endswith(".ifc")]
if not ifc_files:
st.warning("β οΈ Nessun file IFC trovato nella cartella.")
else:
for file in ifc_files:
full_path = os.path.join(folder_path, file)
st.write(f"π€ Caricamento `{file}` su branch `{selected_branch}`...")
try:
model = process_ifc(full_path)
transport = ServerTransport(client=client, stream_id=selected_stream_id)
object_id = operations.send(base=model, transports=[transport])
commit_id = client.commit.create(
stream_id=selected_stream_id,
object_id=object_id,
branch_name=selected_branch,
message=f"Upload di {file} su {selected_branch}"
)
st.success(f"β
`{file}` caricato con successo. Commit ID: {commit_id}")
except Exception as e:
st.error(f"β Errore con `{file}`: {e}")