IFC Universal connector - for workspaces

Hi everyone! :waving_hand:
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!


:toolbox: What this app does

This Streamlit script allows you to:

  • :locked_with_key: Authenticate with your Speckle account using a personal token
  • :globe_with_meridians: Select the target workspace (deduced from stream names)
  • :file_folder: Choose the stream inside that workspace
  • :herb: Select the branch where you want to upload
  • :open_file_folder: Choose a local folder full of IFC files
  • :rocket: 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

:hammer_and_wrench: 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

:locked_with_key: How to get your Speckle token

  1. Go to https://app.speckle.systems
  2. Login and select the correct workspace (top-left menu)
  3. Click your avatar > My Tokens
  4. Create a new token with read/write permissions
  5. Paste the token in the script (or UI, depending on your setup)

:play_button: How to use the app

  1. Launch the app:

bash

CopyEdit

streamlit run ifc_uploader.py
  1. 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! :tada: 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.


:light_bulb: Why this tool is useful

:white_check_mark: Massively speeds up IFC onboarding for new projects or QA pipelines
:white_check_mark: Removes human error from repetitive manual uploads
:white_check_mark: Fully compatible with Speckle Workspaces and multi-branch workflows
:white_check_mark: Easily customizable for additional metadata, Excel checks, or QA rules
:white_check_mark: Speckle-native: uses specklepy and works with commits/branches/streams directly
:white_check_mark: Works on any OS with Python and Streamlit installed
:white_check_mark: Lightweight: no dependencies on Revit, Navisworks, or paid software


:abacus: 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.


:package: 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)

:thread: 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 :smiley:
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}")
7 Likes

Just two pictures of the application and a model uploaded using this script!


6 Likes

Amazing work Oscar, and thanks so much for sharing - I look forward to testing it!!

3 Likes