├── .gitignore ├── MANIFEST.in ├── README.md ├── docs ├── Makefile ├── requirements.txt └── source │ ├── _static │ ├── css │ │ └── readthedocs.css │ └── images │ │ └── luma-logo.png │ ├── conf.py │ └── index.rst ├── lumaapi └── __init__.py ├── pyproject.toml ├── scripts └── run_lumaapi.py └── setup.cfg /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/#use-with-ide 110 | .pdm.toml 111 | 112 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 113 | __pypackages__/ 114 | 115 | # Celery stuff 116 | celerybeat-schedule 117 | celerybeat.pid 118 | 119 | # SageMath parsed files 120 | *.sage.py 121 | 122 | # Environments 123 | .env 124 | .venv 125 | env/ 126 | venv/ 127 | ENV/ 128 | env.bak/ 129 | venv.bak/ 130 | 131 | # Spyder project settings 132 | .spyderproject 133 | .spyproject 134 | 135 | # Rope project settings 136 | .ropeproject 137 | 138 | # mkdocs documentation 139 | /site 140 | 141 | # mypy 142 | .mypy_cache/ 143 | .dmypy.json 144 | dmypy.json 145 | 146 | # Pyre type checker 147 | .pyre/ 148 | 149 | # pytype static type analyzer 150 | .pytype/ 151 | 152 | # Cython debug symbols 153 | cython_debug/ 154 | 155 | # PyCharm 156 | .idea/ -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include MANIFEST.in 3 | include setup.cfg 4 | include pyproject.toml 5 | include lumaapi/__init__.py 6 | include scripts/run_lumaapi.py 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Luma API Python client 2 | 3 | **WARNING:** We are no longer actively supporting this capture API. For Genie API, please contact us for information. 4 | 5 | ### Installation 6 | 7 | `pip install lumaapi` 8 | 9 | ### Docs 10 | 11 | [https://lumalabs.ai/luma-api/client-docs/index.html](https://lumalabs.ai/luma-api/client-docs/index.html) 12 | 13 | To build docs: go to docs/ and 14 | ```sh 15 | make html 16 | ``` 17 | 18 | Need to install requirements first time (`pip install -r docs/requirements.txt`) 19 | 20 | 21 | ### Release 22 | First install deps `pip install python-build twine` 23 | 24 | Then update the version in `pyproject.toml` and 25 | ```sh 26 | python -m build 27 | twine upload dist/lumaapi-.tar.gz 28 | ``` 29 | 30 | For Luma employees: Please get the password from 1Password (search PyPI) 31 | 32 | 33 | ### CLI usage 34 | 35 | - To submit a video: `luma submit `, 36 | where path can be a video, zip, or directory. 37 | - This outputs a slug. 38 | - To check status of the capture: `luma status <slug>` 39 | - To search user's captures: `luma get <title>` 40 | - To manually authenticate: `luma auth` (CLI will also prompt when required) 41 | - To check for credits: `luma credits` 42 | 43 | ### Library usage 44 | ```python 45 | from lumaapi import LumaClient 46 | client = LumaClient(api_key) 47 | slug = client.submit(video_path, title) 48 | print(client.status(slug)) 49 | ``` 50 | 51 | Then use functions corresponding to the CLI 52 | 53 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | sphinx==5.2.1 2 | pytorch_sphinx_theme @ git+https://github.com/liruilong940607/pytorch_sphinx_theme.git#egg=pytorch_sphinx_theme 3 | sphinxcontrib-fulltoc 4 | recommonmark 5 | lumaapi 6 | numpy 7 | enum-tools[sphinx] 8 | sphinx-copybutton==0.5.0 9 | sphinx-design==0.2.0 10 | -------------------------------------------------------------------------------- /docs/source/_static/css/readthedocs.css: -------------------------------------------------------------------------------- 1 | .header-logo { 2 | background-image: url("../images/luma-logo.png"); 3 | background-size: 156px 35px; 4 | height: 35px; 5 | width: 156px; 6 | } 7 | #docs-tutorials-resources { 8 | margin-top: 2.5em; 9 | } 10 | code { 11 | word-break: normal; 12 | } 13 | -------------------------------------------------------------------------------- /docs/source/_static/images/luma-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lumalabs/lumaapi-python/5f785851e7522032e417550d7ed2bb126eabf76d/docs/source/_static/images/luma-logo.png -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | import os 14 | import sys 15 | import subprocess 16 | import pytorch_sphinx_theme 17 | sys.path.insert(0, os.path.join(os.path.abspath('.'), '..')) 18 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = 'lumaapi' 23 | copyright = '2023, Luma AI' 24 | author = 'Luma AI' 25 | 26 | # The full version, including alpha/beta/rc tags 27 | 28 | release = subprocess.check_output(sys.executable + " -m pip show lumaapi | grep Version | cut -d ' ' -f 2", 29 | shell=True).decode('utf-8').strip() 30 | print('lumaapi', release) 31 | 32 | # -- General configuration --------------------------------------------------- 33 | 34 | # Add any Sphinx extension module names here, as strings. They can be 35 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 36 | # ones. 37 | extensions = [ 38 | 'recommonmark', 39 | 'sphinx.ext.napoleon', 40 | 'sphinx.ext.duration', 41 | 'sphinx.ext.doctest', 42 | 'sphinx.ext.autodoc', 43 | 'sphinx.ext.autosummary', 44 | # 'sphinx.ext.intersphinx', 45 | 'pytorch_sphinx_theme', 46 | 'enum_tools.autoenum', 47 | ] 48 | 49 | # Add any paths that contain templates here, relative to this directory. 50 | templates_path = ['_templates'] 51 | 52 | # List of patterns, relative to source directory, that match files and 53 | # directories to ignore when looking for source files. 54 | # This pattern also affects html_static_path and html_extra_path. 55 | exclude_patterns = [] 56 | 57 | 58 | # -- Options for HTML output ------------------------------------------------- 59 | 60 | # The theme to use for HTML and HTML Help pages. See the documentation for 61 | # a list of builtin themes. 62 | # 63 | html_theme = 'pytorch_sphinx_theme' 64 | 65 | # Add any paths that contain custom static files (such as style sheets) here, 66 | # relative to this directory. They are copied after the builtin static files, 67 | # so a file named "default.css" will overwrite the builtin "default.css". 68 | html_theme_path = [pytorch_sphinx_theme.get_html_theme_path()] 69 | html_static_path = ["_static"] 70 | html_css_files = ["css/readthedocs.css"] 71 | 72 | copybutton_prompt_text = r">>> |\.\.\. " 73 | copybutton_prompt_is_regexp = True 74 | 75 | add_module_names = False 76 | 77 | # def skip(app, what, name, obj, would_skip, options): 78 | # if name == "__init__": 79 | # return False 80 | # return would_skip 81 | 82 | # def setup(app): 83 | # app.connect("autodoc-skip-member", skip) 84 | 85 | autodoc_member_order = 'bysource' 86 | 87 | html_theme_options = { 88 | 'collapse_navigation': False, 89 | 'sticky_navigation': True, 90 | "logo_url": "https://lumalabs.ai", 91 | "menu": [ 92 | {"name": "API home", 93 | "url": "https://lumalabs.ai/luma-api"}, 94 | {"name": "API dashboard", 95 | "url": "https://lumalabs.ai/dashboard/api"}, 96 | {"name": "API reference", 97 | "url": "https://documenter.getpostman.com/view/24305418/2s93CRMCas"}, 98 | {"name": "Github", 99 | "url": "https://github.com/lumalabs/lumaapi-python"}, 100 | ], 101 | } 102 | 103 | epub_show_urls = "footnote" 104 | 105 | # typehints 106 | autodoc_typehints = "description" 107 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Luma API Python + CLI Client Reference 2 | ========================================================= 3 | 4 | This is a Python and CLI client for the `Luma API <https://lumalabs.ai/luma-api>`_. 5 | Both are included in the pure-Python library :code:`lumaapi`, which you can install from PyPI: 6 | 7 | .. code-block:: shell 8 | 9 | pip install lumaapi 10 | 11 | We assume you already have Python 3 with pip installed. 12 | 13 | 14 | Example CLI usage 15 | ***************************** 16 | 17 | .. code-block:: shell 18 | 19 | # Check credits 20 | luma credits 21 | # Submit video, zip, or folder (of images). Prints slug 22 | luma submit <path> <title> 23 | # Check status of slug 24 | luma status <slug> 25 | 26 | If not already logged in, you will be prompted 27 | for an API key. You may obtain 28 | one from the `Luma API dashboard <https://lumalabs.ai/dashboard/api>`_. 29 | You may also manually authenticate with 30 | 31 | .. code-block:: shell 32 | 33 | luma auth <api-key> 34 | 35 | 36 | Example usage inside Python 37 | ***************************** 38 | 39 | .. code-block:: python 40 | 41 | from lumaapi import LumaClient 42 | client = LumaClient(api_key) 43 | slug = client.submit(video_path, title) 44 | print(client.status(slug)) 45 | 46 | Again, you may obtain an API key from the `Luma API dashboard <https://lumalabs.ai/dashboard/api>`_. 47 | Any of the functions in LumaClient may be used directly in the CLI e.g. 48 | 49 | .. code-block:: shell 50 | 51 | client.submit(video_path, title) 52 | luma submit video_path title 53 | 54 | Please see detailed per-function documentation below. 55 | 56 | 57 | LumaClient 58 | ***************************** 59 | 60 | .. autoclass:: lumaapi.LumaClient 61 | :members: 62 | 63 | 64 | Misc Types 65 | ***************************** 66 | 67 | .. autoclass:: lumaapi.LumaCreditInfo 68 | :members: 69 | 70 | .. autoclass:: lumaapi.LumaCaptureInfo 71 | :members: 72 | 73 | .. autoclass:: lumaapi.LumaRunInfo 74 | :members: 75 | 76 | .. autoenum:: lumaapi.PrivacyLevel 77 | :members: 78 | 79 | .. autoenum:: lumaapi.CaptureStatus 80 | :members: 81 | 82 | .. autoenum:: lumaapi.RunStatus 83 | :members: 84 | 85 | .. autoenum:: lumaapi.CaptureType 86 | :members: 87 | 88 | .. autoenum:: lumaapi.CameraType 89 | :members: 90 | 91 | .. autoclass:: lumaapi.CaptureLocation 92 | :members: 93 | 94 | Note: 95 | This doc uses `Ruilong Li's fork of the PyTorch Sphinx theme <https://github.com/liruilong940607/pytorch_sphinx_theme>`_ 96 | used for `nerfacc <https://www.nerfacc.com>`_ 97 | -------------------------------------------------------------------------------- /lumaapi/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Luma AI, Inc 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining 4 | # a copy of this software and associated documentation files (the 5 | # "Software"), to deal in the Software without restriction, including 6 | # without limitation the rights to use, copy, modify, merge, publish, 7 | # distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so, subject to 9 | # the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be 12 | # included in all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | import os 23 | import shutil 24 | from typing import Optional, List, Dict 25 | import uuid 26 | import urllib.parse 27 | from enum import Enum 28 | from dataclasses import dataclass 29 | from datetime import datetime 30 | import enum_tools.documentation 31 | import time 32 | import json 33 | import requests 34 | 35 | import platformdirs 36 | 37 | 38 | CACHE_DIR = platformdirs.user_config_dir("luma") 39 | AUTH_FILE = os.path.join(CACHE_DIR, "auth.json") 40 | API_BASE_URL = "https://webapp.engineeringlumalabs.com/api/v2/" 41 | 42 | 43 | @dataclass 44 | class LumaCreditInfo: 45 | """ 46 | Response of credits query 47 | """ 48 | remaining: int 49 | """ 50 | Number of remaining credits 51 | """ 52 | 53 | used: int 54 | """ 55 | Number of used credits 56 | """ 57 | 58 | total: int 59 | """ 60 | Number of remaining+used credits 61 | """ 62 | 63 | @enum_tools.documentation.document_enum 64 | class CaptureType(Enum): 65 | """ 66 | Capture types. 67 | Current API version always has RECONSTRUCTION type 68 | """ 69 | RECONSTRUCTION = 0 # doc: This is the only option 70 | 71 | @classmethod 72 | def parse(cls, name: str) -> "CaptureType": 73 | return getattr(cls, name.upper(), None) 74 | 75 | @enum_tools.documentation.document_enum 76 | class CameraType(Enum): 77 | """ 78 | Camera types 79 | """ 80 | NORMAL = "normal" # doc: Perspective camera 81 | FISHEYE = "fisheye" # doc: Fisheye camera 82 | EQUIRECTANGULAR = "equirectangular" # doc: Equirectangular 360 camera 83 | 84 | @classmethod 85 | def parse(cls, name: str) -> "CameraType": 86 | return getattr(cls, name.upper(), None) 87 | 88 | 89 | @enum_tools.documentation.document_enum 90 | class PrivacyLevel(Enum): 91 | """ 92 | Privacy levels for capture 93 | """ 94 | PRIVATE = "private" # doc: Fully private (default) 95 | UNLISTED = "unlisted" # doc: Unlisted. Sharable by link 96 | PUBLIC = "public" # doc: Shows up in feeds and can be featured 97 | OPEN = "open" # doc: Can be remixed by other users 98 | 99 | @classmethod 100 | def parse(cls, name: str) -> "PrivacyLevel": 101 | return getattr(cls, name.upper(), None) 102 | 103 | @enum_tools.documentation.document_enum 104 | class CaptureStatus(Enum): 105 | """ 106 | Capture upload status. Not to be confused with :class:`.RunStatus` 107 | """ 108 | NEW = 0 # doc: New capture, not uploaded 109 | 110 | UPLOADING = 1 # doc: Capture is uploading 111 | 112 | COMPLETE = 2 # doc: Capture has finished uploading 113 | 114 | @classmethod 115 | def parse(cls, name: str) -> "CaptureStatus": 116 | return getattr(cls, name.upper(), None) 117 | 118 | @dataclass 119 | class CaptureLocation: 120 | """ 121 | Capture location information. 122 | Current API uploads will not have this information. 123 | """ 124 | latitude: float = 0.0 125 | """ 126 | Latitude in deg 127 | """ 128 | longitude: float = 0.0 129 | """ 130 | Longitude in deg 131 | """ 132 | name: str = "" 133 | """ 134 | Name of location if available 135 | """ 136 | is_visible: bool = True 137 | """ 138 | Whether location is visible to other users 139 | """ 140 | 141 | @classmethod 142 | def from_dict(cls, data: Dict) -> "CaptureLocation": 143 | return cls( 144 | latitude=data.get("latitude", 0.0), 145 | longitude=data.get("longitude", 0.0), 146 | name=data.get("name", ""), 147 | is_visible=data.get("is_visible", True)) 148 | 149 | def to_dict(self): 150 | return { 151 | "latitude": self.latitude, 152 | "longitude": self.longitude, 153 | "name": self.name, 154 | "isVisible": self.is_visible 155 | } 156 | 157 | 158 | @enum_tools.documentation.document_enum 159 | class RunStatus(Enum): 160 | """ 161 | Capture run status 162 | """ 163 | NEW = 0 # doc: New run in queue 164 | DISPATCHED = 1 # doc: Run dispatched to worker 165 | FAILED = 2 # doc: Run failed 166 | FINISHED = 3 # doc: Run finished 167 | 168 | @classmethod 169 | def parse(cls, name: str) -> "RunStatus": 170 | return getattr(cls, name.upper(), None) 171 | 172 | 173 | @dataclass 174 | class LumaRunInfo: 175 | status: RunStatus 176 | """ 177 | Status of run 178 | """ 179 | 180 | progress: int 181 | """ 182 | Percentage progress (0-100) 183 | """ 184 | 185 | current_stage: str 186 | """ 187 | Current stage of reconstruction for information. Examples are sfm and nerf 188 | """ 189 | 190 | artifacts: List[Dict[str, str]] 191 | """ 192 | List of output artifacts (each entry has keys type and url) 193 | """ 194 | 195 | @dataclass 196 | class LumaCaptureInfo: 197 | title: str 198 | """ Capture title """ 199 | type: CaptureType 200 | """ Capture type. This will currently be reconstruction """ 201 | location: Optional[CaptureLocation] 202 | """ Location of capture. For API captures, this will be None """ 203 | privacy: PrivacyLevel 204 | """ Capture privacy level """ 205 | date: datetime 206 | """ Capture creation time """ 207 | username: str 208 | """ Username of submitting user """ 209 | status: CaptureStatus 210 | """ Capture upload status """ 211 | latest_run: Optional[LumaRunInfo] 212 | 213 | @classmethod 214 | def from_dict(cls, data: Dict) -> "LumaCaptureInfo": 215 | lrun = data.get("latestRun", None) 216 | return LumaCaptureInfo( 217 | title=data["title"], 218 | type=CaptureType.parse(data["type"]), 219 | location=CaptureLocation.from_dict(data["location"]) if data["location"] is not None else None, 220 | privacy=PrivacyLevel.parse(data["privacy"]), 221 | date=datetime.fromisoformat(data["date"][:-1] + '+00:00'), 222 | username=data["username"], 223 | status=CaptureStatus.parse(data["status"]), 224 | latest_run=LumaRunInfo( 225 | status=RunStatus.parse(lrun["status"]), 226 | progress=lrun["progress"], 227 | current_stage=lrun["currentStage"], 228 | artifacts=lrun["artifacts"], 229 | ) if lrun is not None else None 230 | ) 231 | 232 | 233 | class LumaClient: 234 | """ 235 | Luma API Python Client. Currently limited to basic video/zip/folder uploads and status checking. 236 | 237 | **Library usage:** 238 | 239 | .. code-block:: python 240 | 241 | from lumaapi import LumaClient 242 | client = LumaClient(api_key) 243 | slug = client.submit(video_path, title) 244 | print(client.status(slug)) 245 | 246 | **CLI usage:** 247 | To submit a video 248 | 249 | .. code-block:: shell 250 | 251 | luma submit <path> <title> 252 | 253 | where path can be a video, zip, or directory. 254 | This outputs a slug. 255 | 256 | To check status of the capture 257 | 258 | .. code-block:: shell 259 | 260 | luma status <slug> 261 | 262 | To search user's captures 263 | 264 | .. code-block:: shell 265 | 266 | luma get <title> 267 | 268 | To manually authenticate 269 | (the CLI automatically prompts for api-key when running anything else) 270 | 271 | .. code-block:: shell 272 | 273 | luma auth 274 | 275 | To check for credits 276 | 277 | .. code-block:: shell 278 | 279 | luma credits 280 | 281 | :param api_key: API key. If None, will be requested when needed 282 | :param is_cli: Whether this is being used as a CLI (internal use only) 283 | :param use_cache: Whether to cache the auth headers (default True) 284 | """ 285 | def __init__(self, 286 | api_key: Optional[str] = None, 287 | is_cli: bool = False, 288 | use_cache: bool = True): 289 | """ 290 | Construct LumaClient 291 | """ 292 | self.auth_header = None 293 | self.is_cli = is_cli 294 | self.use_cache = use_cache 295 | if api_key is not None: 296 | self.auth(api_key) 297 | 298 | 299 | def credits(self) -> LumaCreditInfo: 300 | """ 301 | .. code-block:: shell 302 | 303 | luma credits 304 | 305 | Get number of credits remaining for the user. 306 | 307 | :return: LumaCreditInfo 308 | """ 309 | auth_headers = self.auth() 310 | response = requests.get(f"{API_BASE_URL}capture/credits", headers=auth_headers) 311 | response.raise_for_status() 312 | data = response.json() 313 | if self.is_cli: 314 | # Force fire to display it rather than trying to 315 | # run subcommands 316 | return data 317 | return LumaCreditInfo(**data) 318 | 319 | 320 | def auth(self, api_key: Optional[str] = None) -> Dict[str, str]: 321 | """ 322 | .. code-block:: shell 323 | 324 | luma auth [api-key] 325 | 326 | Update the api_key to the provided api_key. 327 | Alternatively, if api_key is not given, load the cached API key, 328 | or ask the user to enter it 329 | If api_key is updated, runs client.credits() to check its validity. 330 | 331 | :param api_key: str, optional, API key to use instead of prompting user 332 | 333 | :return: dict, headers to use for authenticated requests (:code:`Authorization: luma-api-key=<api_key>`) 334 | """ 335 | if api_key is None and self.auth_header is not None: 336 | result = self.auth_header 337 | elif api_key is None and os.path.isfile(AUTH_FILE): 338 | with open(AUTH_FILE, "r") as f: 339 | result = json.load(f) 340 | self.auth_header = result 341 | else: 342 | # Prompt user for API key 343 | if api_key is None: 344 | api_key = input("Enter your Luma API key (get from https://lumalabs.ai/dashboard/api): ").strip() 345 | result = {"Authorization": 'luma-api-key=' + api_key} 346 | if self.use_cache: 347 | os.makedirs(CACHE_DIR, exist_ok=True) 348 | with open(AUTH_FILE, "w") as f: 349 | json.dump(result, f) 350 | 351 | print("Verifying api-key...") 352 | # Check it by getting credits 353 | try: 354 | self.credits() 355 | except Exception as ex: 356 | print("401 invalid API key, please obtain one from https://lumalabs.ai/dashboard/api") 357 | os.remove(AUTH_FILE) 358 | raise ex 359 | self.auth_header = result 360 | 361 | return result 362 | 363 | 364 | def clear_auth(self): 365 | """ 366 | .. code-block:: shell 367 | 368 | luma clear-auth 369 | 370 | Remove cached authorization (:meth:`.auth`) if present 371 | """ 372 | if os.path.isfile(AUTH_FILE): 373 | os.remove(AUTH_FILE) 374 | 375 | 376 | def submit(self, 377 | path: str, 378 | title: str, 379 | cam_model: CameraType = CameraType.NORMAL, 380 | silent: bool = False, 381 | ) -> str: 382 | """ 383 | .. code-block:: shell 384 | 385 | luma submit <path> <title> 386 | 387 | Submit a video, zip, or directory (at path) to Luma for processing, with given title. 388 | User might be prompted for API key, if not already authenticated (call auth). 389 | Returns the slug. After submissing, use status(slug) to check the status 390 | and output artifacts. 391 | 392 | :param path: str, path to video, zip of images, zip of multiple videos, or directory (with images) to submit 393 | :param title: str, a descriptive title for the capture 394 | :param cam_model: CameraType, camera model 395 | :param silent: bool, if True, do not print progress 396 | 397 | :return: str, the slug identifier for checking the status etc 398 | """ 399 | tmp_path = None 400 | if os.path.isdir(path): 401 | path = path.rstrip("/").rstrip("\\") 402 | tmp_path = os.path.join(os.path.dirname(path), 403 | uuid.uuid4().hex) 404 | if not silent: 405 | print("Compressing directory", path, "to", tmp_path + ".zip") 406 | path = shutil.make_archive(tmp_path, 'zip', path) 407 | if not silent: 408 | print("Compressed to", path) 409 | 410 | with open(path, "rb") as f: 411 | payload = f.read() 412 | result = self.submit_binary(payload, title, 413 | cam_model=cam_model, 414 | silent=silent) 415 | if tmp_path is not None and os.path.isfile(tmp_path): 416 | os.remove(tmp_path) 417 | return result 418 | 419 | def submit_binary(self, 420 | payload: bytes, 421 | title: str, 422 | cam_model: CameraType = CameraType.NORMAL, 423 | silent: bool = False, 424 | ) -> str: 425 | """ 426 | **[Python only]** 427 | Submit a video or zip (as binary blob) to Luma for processing, with given title. 428 | User might be prompted for API key, if not already authenticated (call auth). 429 | Returns the slug. After submissing, use status(slug) to check the status 430 | and output artifacts. 431 | 432 | :param payload: bytes, 433 | :param title: str, a descriptive title for the capture 434 | :param cam_model: CameraType, camera model 435 | 436 | :return: str, the slug identifier for checking the status etc 437 | """ 438 | auth_headers = self.auth() 439 | 440 | # 1. Create capture 441 | capture_data = { 442 | 'title': title, 443 | 'camModel': cam_model.value, 444 | } 445 | if not silent: 446 | print("Capture data", capture_data) 447 | response = requests.post(f"{API_BASE_URL}capture", 448 | headers=auth_headers, data=capture_data) 449 | response.raise_for_status() 450 | capture_data = response.json() 451 | upload_url = capture_data['signedUrls']['source'] 452 | slug = capture_data['capture']['slug'] 453 | if not silent: 454 | print("Created capture", slug) 455 | print("Uploading") 456 | 457 | # 2. Upload video or zip 458 | response = requests.put(upload_url, headers={'Content-Type': 'text/plain'}, data=payload) 459 | response.raise_for_status() 460 | 461 | time.sleep(0.5) 462 | if not silent: 463 | print("Triggering") 464 | 465 | # 3. Trigger processing 466 | response = requests.post(f"{API_BASE_URL}capture/{slug}", headers=auth_headers) 467 | response.raise_for_status() 468 | 469 | if not silent: 470 | print("Submitted", slug) 471 | return slug 472 | 473 | 474 | def status(self, slug: str) -> LumaCaptureInfo: 475 | """ 476 | .. code-block:: shell 477 | 478 | luma status <slug> 479 | 480 | Check the status of a submitted capture 481 | 482 | :param slug: str, slug of capture to check (from submit()) 483 | 484 | :return: LumaCaptureInfo dataclass 485 | """ 486 | auth_headers = self.auth() 487 | response = requests.get(f"{API_BASE_URL}capture/{slug}", headers=auth_headers) 488 | response.raise_for_status() 489 | data = response.json() 490 | if self.is_cli: 491 | # Force fire to display it rather than trying to 492 | # run subcommands 493 | return data 494 | return LumaCaptureInfo.from_dict(data) 495 | 496 | 497 | def get(self, 498 | query: str="", 499 | skip : int=0, 500 | take : int=50, 501 | desc : bool = True) -> List[LumaCaptureInfo]: 502 | """ 503 | .. code-block:: shell 504 | 505 | luma get <query> 506 | 507 | Find captures from all of the user's API captures 508 | 509 | :param query: str, query string to filter captures by (title) 510 | :param skip: int, starting capture index 511 | :param take: int, number of captures to take 512 | :param desc: bool, whether to sort in descending order 513 | 514 | :return: list of LumaCaptureInfo dataclass 515 | """ 516 | auth_headers = self.auth() 517 | query = urllib.parse.quote(query) 518 | skip = int(skip) 519 | take = int(take) 520 | order = "DESC" if desc else "ASC" 521 | url = f"{API_BASE_URL}capture?" 522 | if query: 523 | url += f"search={query}&" 524 | response = requests.get(url + f"skip={skip}&take={take}&order={order}", 525 | headers=auth_headers) 526 | response.raise_for_status() 527 | data = response.json() 528 | return [LumaCaptureInfo.from_dict(x) for x in data["captures"]] 529 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["setuptools>=61.0", "build"] 3 | build-backend = "setuptools.build_meta" 4 | 5 | [project] 6 | name = "lumaapi" 7 | version = "0.0.4" 8 | authors = [ 9 | {name = "Luma AI", email = "hello@lumalabs.ai"}, 10 | ] 11 | description = "Luma AI API wrapper library and CLI" 12 | readme = "README.md" 13 | requires-python = ">=3.7" 14 | license = {text = "MIT"} 15 | classifiers = [ 16 | "License :: OSI Approved :: MIT License", 17 | "Programming Language :: Python :: 3", 18 | ] 19 | dependencies = [ 20 | "requests", 21 | "fire", 22 | "enum-tools", 23 | "platformdirs", 24 | ] 25 | 26 | [project.urls] 27 | Homepage = "https://lumalabs.ai/luma-api" 28 | 29 | [tool.setuptools.packages.find] 30 | include = ["lumaapi*", "scripts*"] 31 | 32 | 33 | [project.scripts] 34 | luma = "scripts.run_lumaapi:entrypoint" 35 | -------------------------------------------------------------------------------- /scripts/run_lumaapi.py: -------------------------------------------------------------------------------- 1 | # Copyright 2023 Luma AI, Inc 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining 4 | # a copy of this software and associated documentation files (the 5 | # "Software"), to deal in the Software without restriction, including 6 | # without limitation the rights to use, copy, modify, merge, publish, 7 | # distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so, subject to 9 | # the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be 12 | # included in all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | 22 | from lumaapi import LumaClient 23 | import fire 24 | 25 | def entrypoint(): 26 | fire.Fire(LumaClient(is_cli=True)) 27 | 28 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [isort] 2 | multi_line_output = 3 3 | line_length = 80 4 | include_trailing_comma = true 5 | --------------------------------------------------------------------------------