#!/usr/bin/env python3
# flake8: noqa: E501
# Copyright (c) 2025 ByteDance Ltd. and/or its affiliates
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Depth Anything 3 Gallery Server (two-level, single-file)
Now supports paginated depth preview (4 per page).
"""
import argparse
import json
import mimetypes
import os
import posixpath
import sys
from functools import partial
from http import HTTPStatus
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import quote, unquote
# ------------------------------ Embedded HTML ------------------------------ #
HTML_PAGE = r"""
Depth Anything 3 Gallery
Depth Anything 3 Gallery
Level 1 shows groups only; click a group to browse scenes and previews.
🎯 Depth Anything 3 Gallery
Explore 3D reconstructions and depth visualizations from Depth Anything 3.
Browse through groups of scenes, preview 3D models, and examine depth maps interactively.
No available groups
No available scenes in this group
Loading…
⬇
"""
# ------------------------------ Utilities ------------------------------ #
IMAGE_EXTS = (".png", ".jpg", ".jpeg", ".webp", ".bmp")
def _url_join(*parts: str) -> str:
norm = posixpath.join(*[p.replace("\\", "/") for p in parts])
segs = [s for s in norm.split("/") if s not in ("", ".")]
return "/".join(quote(s) for s in segs)
def _is_plain_name(name: str) -> bool:
return all(c not in name for c in ("/", "\\")) and name not in (".", "..")
def build_group_list(root_dir: str) -> dict:
groups = []
try:
for gname in sorted(os.listdir(root_dir)):
gpath = os.path.join(root_dir, gname)
if not os.path.isdir(gpath):
continue
has_scene = False
try:
for sname in os.listdir(gpath):
spath = os.path.join(gpath, sname)
if not os.path.isdir(spath):
continue
if os.path.exists(os.path.join(spath, "scene.glb")) and os.path.exists(
os.path.join(spath, "scene.jpg")
):
has_scene = True
break
except Exception:
pass
if has_scene:
groups.append({"id": gname, "title": gname})
except Exception as e:
print(f"[warn] build_group_list failed: {e}", file=sys.stderr)
return {"groups": groups}
def build_group_manifest(root_dir: str, group: str) -> dict:
items = []
gpath = os.path.join(root_dir, group)
try:
if not os.path.isdir(gpath):
return {"group": group, "items": []}
for sname in sorted(os.listdir(gpath)):
spath = os.path.join(gpath, sname)
if not os.path.isdir(spath):
continue
glb_fs = os.path.join(spath, "scene.glb")
jpg_fs = os.path.join(spath, "scene.jpg")
if not (os.path.exists(glb_fs) and os.path.exists(jpg_fs)):
continue
depth_images = []
dpath = os.path.join(spath, "depth_vis")
if os.path.isdir(dpath):
files = [
f for f in os.listdir(dpath) if os.path.splitext(f)[1].lower() in IMAGE_EXTS
]
for fn in sorted(files):
depth_images.append("/" + _url_join(group, sname, "depth_vis", fn))
items.append(
{
"id": sname,
"title": sname,
"model": "/" + _url_join(group, sname, "scene.glb"),
"thumbnail": "/" + _url_join(group, sname, "scene.jpg"),
"depth_images": depth_images,
}
)
except Exception as e:
print(f"[warn] build_group_manifest failed for {group}: {e}", file=sys.stderr)
return {"group": group, "items": items}
class GalleryHandler(SimpleHTTPRequestHandler):
def __init__(self, *args, directory=None, **kwargs):
super().__init__(*args, directory=directory, **kwargs)
def do_GET(self):
if self.path in ("/", "/index.html") or self.path.startswith("/?"):
content = HTML_PAGE.encode("utf-8")
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", "text/html; charset=utf-8")
self.send_header("Content-Length", str(len(content)))
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(content)
return
if self.path == "/manifest.json":
data = json.dumps(
build_group_list(self.directory), ensure_ascii=False, indent=2
).encode("utf-8")
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(data)))
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(data)
return
if self.path.startswith("/manifest/") and self.path.endswith(".json"):
group_enc = self.path[len("/manifest/") : -len(".json")]
try:
group = unquote(group_enc)
except Exception:
group = group_enc
if not _is_plain_name(group):
self.send_error(HTTPStatus.BAD_REQUEST, "Invalid group name")
return
data = json.dumps(
build_group_manifest(self.directory, group), ensure_ascii=False, indent=2
).encode("utf-8")
self.send_response(HTTPStatus.OK)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(data)))
self.send_header("Cache-Control", "no-store")
self.end_headers()
self.wfile.write(data)
return
if self.path == "/favicon.ico":
self.send_response(HTTPStatus.NO_CONTENT)
self.end_headers()
return
return super().do_GET()
def list_directory(self, path):
self.send_error(HTTPStatus.NOT_FOUND, "Directory listing disabled")
return None
def gallery():
parser = argparse.ArgumentParser(
description="Depth Anything 3 Gallery Server (two-level, with pagination)"
)
parser.add_argument(
"-d", "--dir", required=True, help="Gallery root directory (two-level: group/scene)"
)
parser.add_argument("-p", "--port", type=int, default=8000, help="Port (default 8000)")
parser.add_argument("--host", default="127.0.0.1", help="Host address (default 127.0.0.1)")
parser.add_argument("--open", action="store_true", help="Open browser after launch")
args = parser.parse_args()
root_dir = os.path.abspath(args.dir)
if not os.path.isdir(root_dir):
print(f"[error] Directory not found: {root_dir}", file=sys.stderr)
sys.exit(1)
Handler = partial(GalleryHandler, directory=root_dir)
server = ThreadingHTTPServer((args.host, args.port), Handler)
addr = f"http://{args.host}:{args.port}/"
print(f"[info] Serving gallery from: {root_dir}")
print(f"[info] Open: {addr}")
if args.open:
try:
import webbrowser
webbrowser.open(addr)
except Exception as e:
print(f"[warn] Failed to open browser: {e}", file=sys.stderr)
try:
server.serve_forever()
except KeyboardInterrupt:
print("\n[info] Shutting down...")
finally:
server.server_close()
def main():
"""Main entry point for gallery server."""
mimetypes.add_type("model/gltf-binary", ".glb")
gallery()
if __name__ == "__main__":
main()