95 lines
6.2 KiB
Python
95 lines
6.2 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
DJI Returns Image Viewer -- Generator
|
|
Run this whenever you add new images to the Images/ folder.
|
|
New folders are auto-detected. Edit CATEGORY_CONFIG to customise labels.
|
|
"""
|
|
import os, json, sys
|
|
|
|
BASE = os.path.dirname(os.path.abspath(__file__))
|
|
IMAGES_DIR = os.path.join(BASE, 'Images')
|
|
OUTPUT_HTML = os.path.join(BASE, 'DJI_Returns_Image_Viewer.html')
|
|
TEMPLATE_FILE = os.path.join(BASE, 'viewer_template.html')
|
|
|
|
CATEGORY_CONFIG = {
|
|
'Acceptable Damage': {'label': 'Acceptable Damage', 'section': 'accept', 'dot': '#2acc94', 'desc': 'Minor wear -- acceptable for resale'},
|
|
'Acceptable Damage/RC': {'label': 'Acceptable -- RC Controllers','section': 'accept', 'dot': '#50f7bd', 'desc': 'Controller scuffs and minor marks'},
|
|
'Excessive Crash Damage': {'label': 'Excessive Crash Damage', 'section': 'reject', 'dot': '#ff4d4d', 'desc': 'Automatic reject'},
|
|
'Excessive Physical Damage': {'label': 'Excessive Physical Damage', 'section': 'reject', 'dot': '#ff6622', 'desc': 'Structural damage'},
|
|
'Fire': {'label': 'Fire / Burn Damage', 'section': 'reject', 'dot': '#ff7700', 'desc': 'Fire or thermal damage'},
|
|
'Mini 3-4Pro Cracked Gimbals': {'label': 'Cracked Gimbals', 'section': 'reject', 'dot': '#ff6b35', 'desc': 'Mini 3 / 4 Pro gimbal damage'},
|
|
'P3 Gimbal Damage': {'label': 'P3 Gimbal Damage', 'section': 'reject', 'dot': '#ff6b35', 'desc': 'Phantom 3 gimbal'},
|
|
'Subtle But Rejectable': {'label': 'Subtle But Rejectable', 'section': 'reject', 'dot': '#ffaa00', 'desc': 'Non-obvious -- still a reject'},
|
|
'Tampering': {'label': 'Tampering / Modified', 'section': 'reject', 'dot': '#cc44ff', 'desc': 'Signs of tampering'},
|
|
'Very Worn': {'label': 'Very Worn', 'section': 'borderline', 'dot': '#ffcc00', 'desc': 'Heavy use -- borderline case'},
|
|
'Water Damage': {'label': 'Water Damage', 'section': 'reject', 'dot': '#3eafff', 'desc': 'Water ingress -- automatic reject'},
|
|
'Strange': {'label': 'Strange / Unusual', 'section': 'reject', 'dot': '#9966cc', 'desc': 'Unusual damage patterns'},
|
|
'Strange/mb_removed': {'label': 'Mainboard Removed', 'section': 'reject', 'dot': '#cc66aa', 'desc': 'Components stripped'},
|
|
'Mavic Mini-4k': {'label': 'Mavic Mini 4K Ref', 'section': 'reference', 'dot': '#4488cc', 'desc': 'Reference images'},
|
|
'Retail Images': {'label': 'All Retail Images', 'section': 'reference', 'dot': '#239aee', 'desc': 'Product retail shots'},
|
|
'Retail Images/Action': {'label': 'Action Cameras', 'section': 'reference', 'dot': '#2acc94', 'desc': 'Action 4 / 5 / 6'},
|
|
'Retail Images/Avata': {'label': 'Avata', 'section': 'reference', 'dot': '#2acc94', 'desc': 'Avata 2 range'},
|
|
'Retail Images/Flip': {'label': 'Flip', 'section': 'reference', 'dot': '#2acc94', 'desc': 'DJI Flip range'},
|
|
'Retail Images/Lito': {'label': 'Lito 1 / X1', 'section': 'reference', 'dot': '#2acc94', 'desc': 'Entry-level 249g drones'},
|
|
'Retail Images/Mini': {'label': 'Mini Series', 'section': 'reference', 'dot': '#2acc94', 'desc': 'Mini 2 / 3 / 4 Pro / 5 Pro'},
|
|
'Retail Images/Neo': {'label': 'Neo', 'section': 'reference', 'dot': '#2acc94', 'desc': 'DJI Neo / Neo 2'},
|
|
'Retail Images/OM': {'label': 'Osmo Mobile', 'section': 'reference', 'dot': '#2acc94', 'desc': 'OM7 / OM7P / OM8 / OM8P'},
|
|
'Retail Images/Osmo 360': {'label': 'Osmo 360', 'section': 'reference', 'dot': '#2acc94', 'desc': 'Osmo 360 camera'},
|
|
'Retail Images/Osmo Nano': {'label': 'Osmo Nano', 'section': 'reference', 'dot': '#2acc94', 'desc': 'Osmo Nano camera'},
|
|
}
|
|
|
|
SECTION_ORDER = ['accept','reject','borderline','reference']
|
|
IMAGE_EXTS = {'.jpg','.jpeg','.png','.webp','.gif','.bmp'}
|
|
|
|
def scan():
|
|
results = []
|
|
if not os.path.isdir(IMAGES_DIR):
|
|
print(f'ERROR: Images folder not found at {IMAGES_DIR}'); sys.exit(1)
|
|
for dirpath, dirnames, filenames in os.walk(IMAGES_DIR):
|
|
dirnames.sort()
|
|
for fname in sorted(filenames):
|
|
if os.path.splitext(fname)[1].lower() in IMAGE_EXTS:
|
|
rel = os.path.relpath(os.path.join(dirpath, fname), BASE).replace('\\\\', '/')
|
|
cat_key = os.path.relpath(dirpath, IMAGES_DIR).replace('\\\\', '/')
|
|
if cat_key == '.': cat_key = ''
|
|
results.append({'path': rel, 'cat': cat_key})
|
|
return results
|
|
|
|
def build_cat_meta(images):
|
|
seen = {}
|
|
for img in images:
|
|
k = img['cat']
|
|
if k not in seen:
|
|
cfg = CATEGORY_CONFIG.get(k)
|
|
if cfg:
|
|
seen[k] = dict(cfg, key=k, count=0)
|
|
else:
|
|
label = os.path.basename(k) if k else 'Uncategorised'
|
|
seen[k] = {'key':k,'label':label,'section':'reference','dot':'#888','desc':'Auto-detected folder','count':0}
|
|
seen[k]['count'] += 1
|
|
def sort_key(c):
|
|
try: si = SECTION_ORDER.index(c['section'])
|
|
except ValueError: si = 99
|
|
return (si, c['label'])
|
|
return sorted(seen.values(), key=sort_key)
|
|
|
|
def main():
|
|
print('Scanning images...')
|
|
images = scan()
|
|
print(f' Found {len(images)} images')
|
|
cats = build_cat_meta(images)
|
|
print(f' Found {len(cats)} categories')
|
|
for c in cats:
|
|
print(f' [{c["section"]:10}] {c["key"]} ({c["count"]} images)')
|
|
with open(TEMPLATE_FILE, 'r', encoding='utf-8') as f:
|
|
template = f.read()
|
|
html = template.replace('__IMAGE_DATA__', json.dumps(images, indent=2))
|
|
html = html.replace('__CAT_DATA__', json.dumps(cats, indent=2))
|
|
with open(OUTPUT_HTML, 'w', encoding='utf-8') as f:
|
|
f.write(html)
|
|
print(f'\nDone! Saved: {OUTPUT_HTML}')
|
|
print(f'Total: {len(images)} images across {len(cats)} categories')
|
|
|
|
if __name__ == '__main__':
|
|
main()
|