File size: 12,043 Bytes
c1fc5b2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c811f2f
 
c1fc5b2
c811f2f
 
 
 
 
 
 
 
c1fc5b2
 
5cb5de0
c1fc5b2
5cb5de0
 
 
 
 
 
 
 
c1fc5b2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
01f651c
 
 
 
 
 
 
 
c1fc5b2
 
01f651c
c1fc5b2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
"""
Centralized Configuration for Path Management

This module provides environment-aware path management to ensure
compatibility between local development and HuggingFace Space deployment.

Usage:
    from code.cube3d.config import DATA_DIR, LABEL_MAPPINGS, get_mapping_paths

    # Get paths for a specific mapping set
    forward_path, inverse_path = get_mapping_paths("subset_1k")
"""

import os
import json
import re
import tempfile
from pathlib import Path
from typing import Dict, Tuple, Optional


# ============================================================================
# Environment Detection
# ============================================================================

def detect_environment() -> str:
    """
    Detect current runtime environment

    Returns:
        "huggingface" if running on HF Space, "local" otherwise
    """
    if os.getenv("SPACE_ID") or os.getenv("SPACE_AUTHOR_NAME"):
        return "huggingface"
    return "local"


# ============================================================================
# Path Configuration
# ============================================================================

ENVIRONMENT = detect_environment()

# Project root detection
if ENVIRONMENT == "huggingface":
    # HuggingFace Space: app runs from /home/user/app
    PROJECT_ROOT = Path("/home/user/app")
else:
    # Local: calculate from this file's location
    # config.py is at: code/cube3d/config.py
    # So PROJECT_ROOT = ../../.. from here
    PROJECT_ROOT = Path(__file__).parent.parent.parent.resolve()

# Data directory
DATA_DIR = PROJECT_ROOT / "data"

# Subdirectories
CAR_1K_DIR = DATA_DIR / "car_1k"
CAR_DATA_DIR = DATA_DIR / "1313个筛选车结构和对照渲染图"

# HuggingFace model cache directory
# CRITICAL: Must match where preload_from_hub downloads models
if ENVIRONMENT == "huggingface":
    # HuggingFace Spaces: Use HF_HUB_CACHE (matches preload_from_hub behavior)
    # preload_from_hub ALWAYS downloads to ~/.cache/huggingface/hub regardless of HF_HOME
    # See: https://huggingface.co/docs/hub/spaces-config-reference
    HF_CACHE_DIR = os.getenv(
        "HF_HUB_CACHE",
        os.path.expanduser("~/.cache/huggingface/hub")
    )
    print(f"✅ [Config] HuggingFace cache directory: {HF_CACHE_DIR}")

else:
    # Local development: use standard user cache
    HF_CACHE_DIR = os.path.expanduser("~/.cache/huggingface")
    try:
        os.makedirs(HF_CACHE_DIR, exist_ok=True)
        print(f"[Config] Local HuggingFace cache directory: {HF_CACHE_DIR}")
    except (PermissionError, OSError) as e:
        # Fallback to temp directory
        HF_CACHE_DIR = os.path.join(tempfile.gettempdir(), "huggingface")
        os.makedirs(HF_CACHE_DIR, exist_ok=True)
        print(f"⚠️  [Config] Using temp directory due to permission error: {HF_CACHE_DIR}")


# ============================================================================
# Label Mapping Paths
# ============================================================================

LABEL_MAPPINGS: Dict[str, Dict[str, Path]] = {
    "subset_self": {
        "forward": CAR_1K_DIR / "subset_self" / "label_mapping.json",
        "inverse": CAR_1K_DIR / "subset_self" / "label_inverse_mapping.json",
    },
    "subset_1k": {
        "forward": CAR_1K_DIR / "subset_1k" / "label_mapping_merge.json",
        "inverse": CAR_1K_DIR / "subset_1k" / "label_inverse_mapping_merge.json",
    },
}

# Runtime-generated mapping cache (for HuggingFace Space with storage limits)
_RUNTIME_MAPPING_CACHE: Dict[str, Tuple[str, str]] = {}


# ============================================================================
# Helper Functions
# ============================================================================

def generate_label_mappings_from_ldr(ldr_dir: Path, mapping_type: str = "subset_1k") -> Tuple[str, str]:
    """
    Generate label mappings by scanning LDR files at runtime

    This is a fallback for HuggingFace Spaces where storage limits prevent
    pre-uploading large mapping files. Mappings are cached in memory.

    Args:
        ldr_dir: Directory containing LDR files
        mapping_type: Type of mapping to generate

    Returns:
        Tuple of (forward_mapping_path, inverse_mapping_path) in /tmp
    """
    print(f"🔧 Generating label mappings from LDR files in {ldr_dir}...")

    # Check cache first
    if mapping_type in _RUNTIME_MAPPING_CACHE:
        print(f"✅ Using cached mappings for {mapping_type}")
        return _RUNTIME_MAPPING_CACHE[mapping_type]

    # Scan LDR files
    label_mapping = {}  # part_name -> ID
    label_inverse_mapping = {}  # ID -> part_name
    label_counter = 0

    ldr_files = list(ldr_dir.glob("**/*.ldr"))
    print(f"📂 Found {len(ldr_files)} LDR files to process")

    for ldr_file in ldr_files:
        try:
            with open(ldr_file, 'r', encoding='utf-8', errors='ignore') as f:
                for line in f:
                    if line.startswith('1'):  # Part data line
                        parts = line.split()
                        if len(parts) < 15:
                            continue

                        # Extract part identifier (lowercase, starting digits)
                        filename = parts[14].lower()
                        match = re.match(r'^\d+', filename)
                        part_identifier = match.group() if match else filename

                        if part_identifier not in label_mapping:
                            label_mapping[part_identifier] = label_counter
                            label_inverse_mapping[label_counter] = part_identifier
                            label_counter += 1
        except Exception as e:
            print(f"⚠️  Error processing {ldr_file}: {e}")
            continue

    print(f"✅ Generated {len(label_mapping)} unique part mappings")

    # Save to /tmp directory
    tmp_dir = Path(tempfile.gettempdir()) / "lego_mappings" / mapping_type
    tmp_dir.mkdir(parents=True, exist_ok=True)

    forward_path = tmp_dir / "label_mapping_merge.json"
    inverse_path = tmp_dir / "label_inverse_mapping_merge.json"

    with open(forward_path, 'w', encoding='utf-8') as f:
        json.dump(label_mapping, f, ensure_ascii=False, indent=2)

    # Convert int keys to str keys for JSON
    inverse_str_keys = {str(k): v for k, v in label_inverse_mapping.items()}
    with open(inverse_path, 'w', encoding='utf-8') as f:
        json.dump(inverse_str_keys, f, ensure_ascii=False, indent=2)

    print(f"💾 Saved mappings to:")
    print(f"   {forward_path}")
    print(f"   {inverse_path}")

    # Cache the paths
    result = (str(forward_path), str(inverse_path))
    _RUNTIME_MAPPING_CACHE[mapping_type] = result

    return result


def get_mapping_paths(mapping_type: str = "subset_1k") -> Tuple[str, str]:
    """
    Get label mapping file paths for a given mapping type

    Automatically generates mappings from LDR files if not found.

    Args:
        mapping_type: Either "subset_self" or "subset_1k"

    Returns:
        Tuple of (forward_mapping_path, inverse_mapping_path) as strings

    Raises:
        ValueError: If mapping_type is invalid
    """
    if mapping_type not in LABEL_MAPPINGS:
        raise ValueError(
            f"Invalid mapping_type: {mapping_type}. "
            f"Must be one of: {list(LABEL_MAPPINGS.keys())}"
        )

    forward_path = LABEL_MAPPINGS[mapping_type]["forward"]
    inverse_path = LABEL_MAPPINGS[mapping_type]["inverse"]

    # Diagnostic logging for HF Spaces debugging
    print(f"🔍 [DEBUG] get_mapping_paths() called for: {mapping_type}")
    print(f"   PROJECT_ROOT: {PROJECT_ROOT}")
    print(f"   Forward path: {forward_path}")
    print(f"   Inverse path: {inverse_path}")
    print(f"   Forward exists: {forward_path.exists()}")
    print(f"   Inverse exists: {inverse_path.exists()}")

    # Check if files exist
    if forward_path.exists() and inverse_path.exists():
        print(f"   ✅ Both files exist, returning paths")
        return str(forward_path), str(inverse_path)

    # Files don't exist - generate from LDR files as fallback
    print(f"⚠️  Label mapping files not found for {mapping_type}")
    print(f"   Missing: {forward_path}")
    print(f"   Missing: {inverse_path}")
    print(f"🔄 Generating label mappings from LDR files (this may take 1-2 minutes)...")

    # Determine LDR directory to scan
    if mapping_type == "subset_1k":
        ldr_dir = CAR_DATA_DIR / "ldr"
        if not ldr_dir.exists():
            ldr_dir = CAR_DATA_DIR  # Try parent directory
    else:
        ldr_dir = CAR_1K_DIR / mapping_type

    if not ldr_dir.exists():
        raise FileNotFoundError(
            f"Cannot generate mappings: LDR directory not found: {ldr_dir}\n"
            f"Please ensure LDR files are available."
        )

    return generate_label_mappings_from_ldr(ldr_dir, mapping_type)


def create_default_mappings(mapping_type: str = "subset_1k") -> Tuple[Dict, Dict]:
    """
    Create minimal default label mappings if files are missing

    This is a fallback for development/testing. Production should have real files.

    Args:
        mapping_type: Mapping type identifier

    Returns:
        Tuple of (label_mapping, label_inverse_mapping) dictionaries
    """
    print(f"⚠️  WARNING: Creating default empty mappings for {mapping_type}")
    print("   This is for fallback only. Production should have real mapping files.")

    # Minimal mapping structure
    label_mapping = {}
    label_inverse_mapping = {}

    return label_mapping, label_inverse_mapping


def load_mappings_safe(mapping_type: str = "subset_1k") -> Tuple[Dict, Dict]:
    """
    Safely load label mappings with fallback

    Attempts to load from files, falls back to defaults if missing.

    Args:
        mapping_type: Either "subset_self" or "subset_1k"

    Returns:
        Tuple of (label_mapping, label_inverse_mapping) dictionaries
    """
    try:
        forward_path, inverse_path = get_mapping_paths(mapping_type)

        with open(forward_path, 'r', encoding='utf-8') as f:
            label_mapping = json.load(f)

        with open(inverse_path, 'r', encoding='utf-8') as f:
            label_inverse_mapping = json.load(f)

        return label_mapping, label_inverse_mapping

    except FileNotFoundError as e:
        print(f"⚠️  {e}")
        return create_default_mappings(mapping_type)


# ============================================================================
# Debug Information
# ============================================================================

def print_config_info():
    """Print current configuration for debugging"""
    print("=" * 60)
    print("Configuration Information")
    print("=" * 60)
    print(f"Environment: {ENVIRONMENT}")
    print(f"Project Root: {PROJECT_ROOT}")
    print(f"Data Directory: {DATA_DIR}")
    print(f"Data Dir Exists: {DATA_DIR.exists()}")
    print("\nLabel Mapping Paths:")
    for mapping_type, paths in LABEL_MAPPINGS.items():
        print(f"\n  {mapping_type}:")
        for key, path in paths.items():
            exists = "✅" if path.exists() else "❌"
            print(f"    {key}: {exists} {path}")
    print("=" * 60)


# ============================================================================
# Module Test
# ============================================================================

if __name__ == "__main__":
    print_config_info()

    # Test loading mappings
    print("\n\nTesting mapping load:")
    try:
        forward, inverse = get_mapping_paths("subset_1k")
        print(f"✅ subset_1k paths retrieved successfully")
        print(f"   Forward: {forward}")
        print(f"   Inverse: {inverse}")
    except Exception as e:
        print(f"❌ Error: {e}")

    try:
        forward, inverse = get_mapping_paths("subset_self")
        print(f"✅ subset_self paths retrieved successfully")
        print(f"   Forward: {forward}")
        print(f"   Inverse: {inverse}")
    except Exception as e:
        print(f"❌ Error: {e}")