Spaces:
Running
on
Zero
Running
on
Zero
Permalink Update
Browse files- app.py +88 -9
- requirements.txt +3 -3
- utils/constants.py +6 -0
- utils/file_utils.py +1 -20
- utils/storage.py +127 -0
app.py
CHANGED
|
@@ -34,9 +34,12 @@ import gc
|
|
| 34 |
# Import functions from modules
|
| 35 |
from utils.file_utils import (
|
| 36 |
cleanup_temp_files,
|
| 37 |
-
get_file_parts
|
| 38 |
-
generate_permalink_from_urls
|
| 39 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
|
| 41 |
from utils.color_utils import (
|
| 42 |
hex_to_rgb,
|
|
@@ -133,6 +136,7 @@ user_dir = constants.TMPDIR
|
|
| 133 |
lora_models = get_lora_models()
|
| 134 |
selected_index = gr.State(value=-1)
|
| 135 |
#html_versions = version_info.versions_html()
|
|
|
|
| 136 |
|
| 137 |
image_processor: Optional[DPTImageProcessor] = None
|
| 138 |
depth_model: Optional[DPTForDepthEstimation] = None
|
|
@@ -804,7 +808,7 @@ def load_trellis_model():
|
|
| 804 |
loaded = False
|
| 805 |
if TRELLIS_PIPELINE == None:
|
| 806 |
try:
|
| 807 |
-
TRELLIS_PIPELINE = TrellisImageTo3DPipeline.from_pretrained("
|
| 808 |
TRELLIS_PIPELINE.cuda()
|
| 809 |
# Preload with a dummy image to finalize initialization
|
| 810 |
try:
|
|
@@ -1175,6 +1179,42 @@ def update_permalink(glb, gaussian, depth_out, depth_src_file):
|
|
| 1175 |
permalink = generate_permalink_from_urls(smallest_file, depth_out, depth_src_file)
|
| 1176 |
return gr.update(visible=True), permalink
|
| 1177 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1178 |
@spaces.GPU()
|
| 1179 |
def getVersions():
|
| 1180 |
#return html_versions
|
|
@@ -1595,9 +1635,28 @@ with gr.Blocks(css_paths="style_20250314.css", title=title, theme='Surn/beeuty',
|
|
| 1595 |
gr.Markdown("""
|
| 1596 |
### Files over 10 MB may not display in the 3D model viewer
|
| 1597 |
""", elem_id="file_size_info", elem_classes="intro" )
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1598 |
with gr.Row(visible=False) as permalink_row:
|
| 1599 |
-
|
| 1600 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1601 |
|
| 1602 |
is_multiimage = gr.State(False)
|
| 1603 |
output_buf = gr.State()
|
|
@@ -1847,8 +1906,8 @@ with gr.Blocks(css_paths="style_20250314.css", title=title, theme='Surn/beeuty',
|
|
| 1847 |
outputs=[glb_file]
|
| 1848 |
).then(
|
| 1849 |
fn=update_permalink,
|
| 1850 |
-
inputs=[glb_file, gaussian_file, depth_output, ddd_image_path], #
|
| 1851 |
-
outputs=[permalink_row,
|
| 1852 |
)
|
| 1853 |
|
| 1854 |
extract_gaussian_btn.click(
|
|
@@ -1861,9 +1920,29 @@ with gr.Blocks(css_paths="style_20250314.css", title=title, theme='Surn/beeuty',
|
|
| 1861 |
).then(
|
| 1862 |
fn=update_permalink,
|
| 1863 |
inputs=[glb_file, gaussian_file, depth_output, ddd_image_path], # Use the actual depth source file here
|
| 1864 |
-
outputs=[permalink_row,
|
| 1865 |
)
|
| 1866 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1867 |
if __name__ == "__main__":
|
| 1868 |
constants.load_env_vars(constants.dotenv_path)
|
| 1869 |
logging.basicConfig(
|
|
@@ -1896,7 +1975,7 @@ if __name__ == "__main__":
|
|
| 1896 |
# image_processor = DPTImageProcessor.from_pretrained("Intel/dpt-large")
|
| 1897 |
# depth_model = DPTForDepthEstimation.from_pretrained("Intel/dpt-large", ignore_mismatched_sizes=True)
|
| 1898 |
if constants.IS_SHARED_SPACE:
|
| 1899 |
-
TRELLIS_PIPELINE = TrellisImageTo3DPipeline.from_pretrained("
|
| 1900 |
TRELLIS_PIPELINE.to(device)
|
| 1901 |
try:
|
| 1902 |
TRELLIS_PIPELINE.preprocess_image(Image.fromarray(np.zeros((512, 512, 3), dtype=np.uint8)), 512, True) # Preload rembg
|
|
|
|
| 34 |
# Import functions from modules
|
| 35 |
from utils.file_utils import (
|
| 36 |
cleanup_temp_files,
|
| 37 |
+
get_file_parts
|
|
|
|
| 38 |
)
|
| 39 |
+
from utils.storage import (
|
| 40 |
+
generate_permalink_from_urls,
|
| 41 |
+
upload_files_to_repo,
|
| 42 |
+
)
|
| 43 |
|
| 44 |
from utils.color_utils import (
|
| 45 |
hex_to_rgb,
|
|
|
|
| 136 |
lora_models = get_lora_models()
|
| 137 |
selected_index = gr.State(value=-1)
|
| 138 |
#html_versions = version_info.versions_html()
|
| 139 |
+
default_folder = "saved_models/3d_model_" + format(random.randint(1, 999999), "06d")
|
| 140 |
|
| 141 |
image_processor: Optional[DPTImageProcessor] = None
|
| 142 |
depth_model: Optional[DPTForDepthEstimation] = None
|
|
|
|
| 808 |
loaded = False
|
| 809 |
if TRELLIS_PIPELINE == None:
|
| 810 |
try:
|
| 811 |
+
TRELLIS_PIPELINE = TrellisImageTo3DPipeline.from_pretrained("Surn/TRELLIS-image-large")
|
| 812 |
TRELLIS_PIPELINE.cuda()
|
| 813 |
# Preload with a dummy image to finalize initialization
|
| 814 |
try:
|
|
|
|
| 1179 |
permalink = generate_permalink_from_urls(smallest_file, depth_out, depth_src_file)
|
| 1180 |
return gr.update(visible=True), permalink
|
| 1181 |
|
| 1182 |
+
def create_permalink(glb, gaussian, depth_out, depth_src_file, folder_name_permalink):
|
| 1183 |
+
"""
|
| 1184 |
+
create a permalink for the 3D model and depth image.
|
| 1185 |
+
Args:
|
| 1186 |
+
glb (str): Path to the extracted GLB file.
|
| 1187 |
+
gaussian (str): Path to the extracted Gaussian file.
|
| 1188 |
+
depth_out (str): File path for the depth image.
|
| 1189 |
+
depth_src_file (str): File path for the image used to generate the 3D model.
|
| 1190 |
+
folder_path (str): The folder path where the files are stored.
|
| 1191 |
+
Returns:
|
| 1192 |
+
str: The permalink URL.
|
| 1193 |
+
"""
|
| 1194 |
+
file_candidates = {}
|
| 1195 |
+
if glb and os.path.exists(glb):
|
| 1196 |
+
file_candidates[glb] = os.path.getsize(glb)
|
| 1197 |
+
if gaussian and os.path.exists(gaussian):
|
| 1198 |
+
file_candidates[gaussian] = os.path.getsize(gaussian)
|
| 1199 |
+
|
| 1200 |
+
# if not file_candidates:
|
| 1201 |
+
# return gr.update(visible=False), ""
|
| 1202 |
+
smallest_file = min(file_candidates, key=file_candidates.get)
|
| 1203 |
+
|
| 1204 |
+
permalink = upload_files_to_repo(
|
| 1205 |
+
files=[smallest_file, depth_out, depth_src_file],
|
| 1206 |
+
repo_id="Surn/Storage",
|
| 1207 |
+
folder_name=folder_name_permalink,
|
| 1208 |
+
create_permalink=True,
|
| 1209 |
+
repo_type="dataset"
|
| 1210 |
+
)[1]
|
| 1211 |
+
return permalink
|
| 1212 |
+
|
| 1213 |
+
def open_permalink(url: str) -> str:
|
| 1214 |
+
if url and url.strip():
|
| 1215 |
+
return f'<script>window.open("{url}", "_blank");</script>'
|
| 1216 |
+
return '<script>alert("Permalink textbox is empty!");</script>'
|
| 1217 |
+
|
| 1218 |
@spaces.GPU()
|
| 1219 |
def getVersions():
|
| 1220 |
#return html_versions
|
|
|
|
| 1635 |
gr.Markdown("""
|
| 1636 |
### Files over 10 MB may not display in the 3D model viewer
|
| 1637 |
""", elem_id="file_size_info", elem_classes="intro" )
|
| 1638 |
+
|
| 1639 |
+
# New UI elements for general file upload and permalink generation
|
| 1640 |
+
with gr.Column(scale=1):
|
| 1641 |
+
folder_name_permalink = gr.Textbox(
|
| 1642 |
+
label="Folder Name in Permalink Repo",
|
| 1643 |
+
placeholder="e.g., my_cool_model_set",
|
| 1644 |
+
value=default_folder, # Default folder name
|
| 1645 |
+
elem_classes="solid small centered"
|
| 1646 |
+
)
|
| 1647 |
+
permalink_button = gr.Button(
|
| 1648 |
+
"Generate Permalink with Folder Name in Repo",
|
| 1649 |
+
elem_classes="solid small centered"
|
| 1650 |
+
)
|
| 1651 |
with gr.Row(visible=False) as permalink_row:
|
| 1652 |
+
permalink = gr.Textbox(
|
| 1653 |
+
label="Viewer Permalink",
|
| 1654 |
+
show_copy_button=True,
|
| 1655 |
+
elem_classes="solid small centered",
|
| 1656 |
+
max_lines=3
|
| 1657 |
+
)
|
| 1658 |
+
view_permalink_button = gr.Button("View in Permalink", elem_classes="solid small centered", variant="secondary")
|
| 1659 |
+
hidden_html = gr.HTML(visible=False)
|
| 1660 |
|
| 1661 |
is_multiimage = gr.State(False)
|
| 1662 |
output_buf = gr.State()
|
|
|
|
| 1906 |
outputs=[glb_file]
|
| 1907 |
).then(
|
| 1908 |
fn=update_permalink,
|
| 1909 |
+
inputs=[glb_file, gaussian_file, depth_output, ddd_image_path], # Use the actual depth source file here
|
| 1910 |
+
outputs=[permalink_row, permalink] # This updates the GLB/Gaussian specific permalink
|
| 1911 |
)
|
| 1912 |
|
| 1913 |
extract_gaussian_btn.click(
|
|
|
|
| 1920 |
).then(
|
| 1921 |
fn=update_permalink,
|
| 1922 |
inputs=[glb_file, gaussian_file, depth_output, ddd_image_path], # Use the actual depth source file here
|
| 1923 |
+
outputs=[permalink_row, permalink] # This updates the GLB/Gaussian specific permalink
|
| 1924 |
)
|
| 1925 |
|
| 1926 |
+
# Create a permalink based on the current model, images, and folder name.
|
| 1927 |
+
permalink_button.click(
|
| 1928 |
+
fn=create_permalink,
|
| 1929 |
+
inputs=[glb_file, gaussian_file, depth_output, ddd_image_path, folder_name_permalink],
|
| 1930 |
+
outputs=[permalink],
|
| 1931 |
+
scroll_to_output=True
|
| 1932 |
+
).then(
|
| 1933 |
+
lambda link: gr.update(visible=True) if link and len(link) > 0 else gr.update(visible=False),
|
| 1934 |
+
inputs=[permalink],
|
| 1935 |
+
outputs=[permalink_row]
|
| 1936 |
+
)
|
| 1937 |
+
|
| 1938 |
+
view_permalink_button.click(
|
| 1939 |
+
fn=open_permalink,
|
| 1940 |
+
inputs=[permalink],
|
| 1941 |
+
outputs=[hidden_html],
|
| 1942 |
+
show_progress=False
|
| 1943 |
+
)
|
| 1944 |
+
|
| 1945 |
+
|
| 1946 |
if __name__ == "__main__":
|
| 1947 |
constants.load_env_vars(constants.dotenv_path)
|
| 1948 |
logging.basicConfig(
|
|
|
|
| 1975 |
# image_processor = DPTImageProcessor.from_pretrained("Intel/dpt-large")
|
| 1976 |
# depth_model = DPTForDepthEstimation.from_pretrained("Intel/dpt-large", ignore_mismatched_sizes=True)
|
| 1977 |
if constants.IS_SHARED_SPACE:
|
| 1978 |
+
TRELLIS_PIPELINE = TrellisImageTo3DPipeline.from_pretrained("Surn/TRELLIS-image-large")
|
| 1979 |
TRELLIS_PIPELINE.to(device)
|
| 1980 |
try:
|
| 1981 |
TRELLIS_PIPELINE.preprocess_image(Image.fromarray(np.zeros((512, 512, 3), dtype=np.uint8)), 512, True) # Preload rembg
|
requirements.txt
CHANGED
|
@@ -73,6 +73,6 @@ https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.3/flash_attn
|
|
| 73 |
https://huggingface.co/spaces/Surn/HexaGrid/resolve/main/wheels/diff_gaussian_rasterization-0.0.0-cp310-cp310-linux_x86_64.whl?download=true
|
| 74 |
https://huggingface.co/spaces/Surn/Hexagrid/resolve/main/wheels/nvdiffrast-0.3.3-cp310-cp310-linux_x86_64.whl?download=true
|
| 75 |
#Windows only
|
| 76 |
-
#https://huggingface.co/spaces/Surn/HexaGrid/main/wheels/flash_attn-2.7.4.post1-cp312-cp312-win_amd64.whl?download=true
|
| 77 |
-
#https://huggingface.co/spaces/Surn/HexaGrid/main/wheels/diff_gaussian_rasterization-0.0.0-cp312-cp312-win_amd64.whl?download=true
|
| 78 |
-
#https://huggingface.co/spaces/Surn/HexaGrid/main/wheels/nvdiffrast-0.3.3-py3-none-any.whl
|
|
|
|
| 73 |
https://huggingface.co/spaces/Surn/HexaGrid/resolve/main/wheels/diff_gaussian_rasterization-0.0.0-cp310-cp310-linux_x86_64.whl?download=true
|
| 74 |
https://huggingface.co/spaces/Surn/Hexagrid/resolve/main/wheels/nvdiffrast-0.3.3-cp310-cp310-linux_x86_64.whl?download=true
|
| 75 |
#Windows only
|
| 76 |
+
#https://huggingface.co/spaces/Surn/HexaGrid/resolve/main/wheels/flash_attn-2.7.4.post1-cp312-cp312-win_amd64.whl?download=true
|
| 77 |
+
#https://huggingface.co/spaces/Surn/HexaGrid/resolve/main/wheels/diff_gaussian_rasterization-0.0.0-cp312-cp312-win_amd64.whl?download=true
|
| 78 |
+
#https://huggingface.co/spaces/Surn/HexaGrid/resolve/main/wheels/nvdiffrast-0.3.3-py3-none-any.whl
|
utils/constants.py
CHANGED
|
@@ -67,6 +67,12 @@ SCALE_FACTOR = (12/5)
|
|
| 67 |
TMPDIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'tmp')
|
| 68 |
os.makedirs(TMPDIR, exist_ok=True)
|
| 69 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
|
| 71 |
PROMPTS = {
|
| 72 |
"Mecha Wasteland Arena": "Regional overhead view, directly from above, centered on the map, orthographic Mecha battlefield map. post-industrial wasteland with crumbling structures, volcanic ridges, scrapyards, and ash plains. Features elevated overwatch positions for long-range combat and tight brawling areas for close-quarters engagements. Partial edge hexes are black. Colors: red, gray, muted orange, ash white, dark brown.",
|
|
|
|
| 67 |
TMPDIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'tmp')
|
| 68 |
os.makedirs(TMPDIR, exist_ok=True)
|
| 69 |
|
| 70 |
+
model_extensions = {".glb", ".gltf", ".obj", ".ply"}
|
| 71 |
+
model_extensions_list = list(model_extensions)
|
| 72 |
+
image_extensions = {".png", ".jpg", ".jpeg", ".webp"}
|
| 73 |
+
image_extensions_list = list(image_extensions)
|
| 74 |
+
upload_file_types = model_extensions_list + image_extensions_list
|
| 75 |
+
|
| 76 |
|
| 77 |
PROMPTS = {
|
| 78 |
"Mecha Wasteland Arena": "Regional overhead view, directly from above, centered on the map, orthographic Mecha battlefield map. post-industrial wasteland with crumbling structures, volcanic ridges, scrapyards, and ash plains. Features elevated overwatch positions for long-range combat and tight brawling areas for close-quarters engagements. Partial edge hexes are black. Colors: red, gray, muted orange, ash white, dark brown.",
|
utils/file_utils.py
CHANGED
|
@@ -103,23 +103,4 @@ def get_unique_file_path(directory, filename, file_ext, counter=0):
|
|
| 103 |
return get_unique_file_path(directory, filename, file_ext, counter + 1)
|
| 104 |
|
| 105 |
# Example usage:
|
| 106 |
-
# new_file_path = get_unique_file_path(video_dir, title_file_name, video_new_ext)
|
| 107 |
-
|
| 108 |
-
def generate_permalink_from_urls(model_url, hm_url, img_url, permalink_viewer_url="surn-3d-viewer.hf.space"):
|
| 109 |
-
"""
|
| 110 |
-
Constructs and returns a permalink URL with query string parameters for the viewer.
|
| 111 |
-
Each parameter is passed separately so that the image positions remain consistent.
|
| 112 |
-
|
| 113 |
-
Parameters:
|
| 114 |
-
model_url (str): Processed URL for the 3D model.
|
| 115 |
-
hm_url (str): Processed URL for the height map image.
|
| 116 |
-
img_url (str): Processed URL for the main image.
|
| 117 |
-
permalink_viewer_url (str): The base viewer URL.
|
| 118 |
-
|
| 119 |
-
Returns:
|
| 120 |
-
str: The generated permalink URL.
|
| 121 |
-
"""
|
| 122 |
-
import urllib.parse
|
| 123 |
-
params = {"3d": model_url, "hm": hm_url, "image": img_url}
|
| 124 |
-
query_str = urllib.parse.urlencode(params)
|
| 125 |
-
return f"https://{permalink_viewer_url}/?{query_str}"
|
|
|
|
| 103 |
return get_unique_file_path(directory, filename, file_ext, counter + 1)
|
| 104 |
|
| 105 |
# Example usage:
|
| 106 |
+
# new_file_path = get_unique_file_path(video_dir, title_file_name, video_new_ext)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
utils/storage.py
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# utils/storage.py
|
| 2 |
+
import os
|
| 3 |
+
import urllib.parse
|
| 4 |
+
import tempfile
|
| 5 |
+
import shutil
|
| 6 |
+
from huggingface_hub import login, upload_folder
|
| 7 |
+
from utils.constants import HF_API_TOKEN, upload_file_types, model_extensions, image_extensions
|
| 8 |
+
|
| 9 |
+
def generate_permalink(valid_files, base_url_external, permalink_viewer_url="surn-3d-viewer.hf.space"):
|
| 10 |
+
"""
|
| 11 |
+
Given a list of valid files, checks if they contain exactly 1 model file and 2 image files.
|
| 12 |
+
Constructs and returns a permalink URL with query parameters if the criteria is met.
|
| 13 |
+
Otherwise, returns None.
|
| 14 |
+
"""
|
| 15 |
+
model_link = None
|
| 16 |
+
images_links = []
|
| 17 |
+
for f in valid_files:
|
| 18 |
+
filename = os.path.basename(f)
|
| 19 |
+
ext = os.path.splitext(filename)[1].lower()
|
| 20 |
+
if ext in model_extensions:
|
| 21 |
+
if model_link is None:
|
| 22 |
+
model_link = f"{base_url_external}/{filename}"
|
| 23 |
+
elif ext in image_extensions:
|
| 24 |
+
images_links.append(f"{base_url_external}/{filename}")
|
| 25 |
+
if model_link and len(images_links) == 2:
|
| 26 |
+
# Construct a permalink to the viewer project with query parameters.
|
| 27 |
+
permalink_viewer_url = f"https://{permalink_viewer_url}/"
|
| 28 |
+
params = {"3d": model_link, "hm": images_links[0], "image": images_links[1]}
|
| 29 |
+
query_str = urllib.parse.urlencode(params)
|
| 30 |
+
return f"{permalink_viewer_url}?{query_str}"
|
| 31 |
+
return None
|
| 32 |
+
|
| 33 |
+
def generate_permalink_from_urls(model_url, hm_url, img_url, permalink_viewer_url="surn-3d-viewer.hf.space"):
|
| 34 |
+
"""
|
| 35 |
+
Constructs and returns a permalink URL with query string parameters for the viewer.
|
| 36 |
+
Each parameter is passed separately so that the image positions remain consistent.
|
| 37 |
+
|
| 38 |
+
Parameters:
|
| 39 |
+
model_url (str): Processed URL for the 3D model.
|
| 40 |
+
hm_url (str): Processed URL for the height map image.
|
| 41 |
+
img_url (str): Processed URL for the main image.
|
| 42 |
+
permalink_viewer_url (str): The base viewer URL.
|
| 43 |
+
|
| 44 |
+
Returns:
|
| 45 |
+
str: The generated permalink URL.
|
| 46 |
+
"""
|
| 47 |
+
import urllib.parse
|
| 48 |
+
params = {"3d": model_url, "hm": hm_url, "image": img_url}
|
| 49 |
+
query_str = urllib.parse.urlencode(params)
|
| 50 |
+
return f"https://{permalink_viewer_url}/?{query_str}"
|
| 51 |
+
|
| 52 |
+
def upload_files_to_repo(files, repo_id, folder_name, create_permalink=False, repo_type="dataset", permalink_viewer_url="surn-3d-viewer.hf.space"):
|
| 53 |
+
"""
|
| 54 |
+
Uploads multiple files to a Hugging Face repository using a batch upload approach via upload_folder.
|
| 55 |
+
|
| 56 |
+
Parameters:
|
| 57 |
+
files (list): A list of file paths (str) to upload.
|
| 58 |
+
repo_id (str): The repository ID on Hugging Face for storage, e.g. "Surn/Storage".
|
| 59 |
+
folder_name (str): The subfolder within the repository where files will be saved.
|
| 60 |
+
create_permalink (bool): If True and if exactly three files are uploaded (1 model and 2 images),
|
| 61 |
+
returns a single permalink to the project with query parameters.
|
| 62 |
+
Otherwise, returns individual permalinks for each file.
|
| 63 |
+
repo_type (str): Repository type ("space", "dataset", etc.). Default is "dataset".
|
| 64 |
+
|
| 65 |
+
Returns:
|
| 66 |
+
If create_permalink is True and files match the criteria:
|
| 67 |
+
tuple: (response, permalink) where response is the output of the batch upload
|
| 68 |
+
and permalink is the URL string (with fully qualified file paths) for the project.
|
| 69 |
+
Otherwise:
|
| 70 |
+
list: A list of tuples (response, permalink) for each file.
|
| 71 |
+
"""
|
| 72 |
+
# Log in using the HF API token.
|
| 73 |
+
login(token=HF_API_TOKEN)
|
| 74 |
+
|
| 75 |
+
valid_files = []
|
| 76 |
+
|
| 77 |
+
# Ensure folder_name does not have a trailing slash.
|
| 78 |
+
folder_name = folder_name.rstrip("/")
|
| 79 |
+
|
| 80 |
+
# Filter for valid files based on allowed extensions.
|
| 81 |
+
for f in files:
|
| 82 |
+
file_name = f if isinstance(f, str) else f.name if hasattr(f, "name") else None
|
| 83 |
+
if file_name is None:
|
| 84 |
+
continue
|
| 85 |
+
ext = os.path.splitext(file_name)[1].lower()
|
| 86 |
+
if ext in upload_file_types:
|
| 87 |
+
valid_files.append(f)
|
| 88 |
+
|
| 89 |
+
if not valid_files:
|
| 90 |
+
return [] # or raise an exception
|
| 91 |
+
|
| 92 |
+
# Create a temporary directory; copy valid files directly into it.
|
| 93 |
+
with tempfile.TemporaryDirectory() as temp_dir:
|
| 94 |
+
for file_path in valid_files:
|
| 95 |
+
filename = os.path.basename(file_path)
|
| 96 |
+
dest_path = os.path.join(temp_dir, filename)
|
| 97 |
+
shutil.copy(file_path, dest_path)
|
| 98 |
+
|
| 99 |
+
# Batch upload all files in the temporary folder.
|
| 100 |
+
# Files will be uploaded under the folder (path_in_repo) given by folder_name.
|
| 101 |
+
response = upload_folder(
|
| 102 |
+
folder_path=temp_dir,
|
| 103 |
+
repo_id=repo_id,
|
| 104 |
+
repo_type=repo_type,
|
| 105 |
+
path_in_repo=folder_name,
|
| 106 |
+
commit_message="Batch upload files"
|
| 107 |
+
)
|
| 108 |
+
|
| 109 |
+
# Construct external URLs for each uploaded file.
|
| 110 |
+
# For datasets, files are served at:
|
| 111 |
+
# https://huggingface.co/datasets/<repo_id>/resolve/main/<folder_name>/<filename>
|
| 112 |
+
base_url_external = f"https://huggingface.co/datasets/{repo_id}/resolve/main/{folder_name}"
|
| 113 |
+
individual_links = []
|
| 114 |
+
for file_path in valid_files:
|
| 115 |
+
filename = os.path.basename(file_path)
|
| 116 |
+
link = f"{base_url_external}/{filename}"
|
| 117 |
+
individual_links.append(link)
|
| 118 |
+
|
| 119 |
+
# If permalink creation is requested and exactly 3 valid files are provided,
|
| 120 |
+
# try to generate a permalink using generate_permalink().
|
| 121 |
+
if create_permalink and len(valid_files) == 3:
|
| 122 |
+
permalink = generate_permalink_from_urls(individual_links[0], individual_links[1], individual_links[2], permalink_viewer_url)
|
| 123 |
+
if permalink:
|
| 124 |
+
return [response, permalink]
|
| 125 |
+
|
| 126 |
+
# Otherwise, return individual tuples for each file.
|
| 127 |
+
return [(response, link) for link in individual_links]
|