"""Model candidates are built away from the live repository; publication is separate."""
from __future__ import annotations
import json
import copy
import os
from pathlib import Path
import re
import shutil
import time
import uuid
import urllib.request
from .core import absolute, identifier, require, KitError, docker, image_id, run, sha256, write_json, read_json, atomic_write, now, lock

PROFILES = {
    'medium': {'name':'RFDETRMedium','outputs':{'dets':[300,4],'labels':[300,91]},
               'source_onnx':'6ab93d8f1c64c369b2273b91738d554ce76466fb31f41f5fabb1f61d24a91999'},
    'pose': {'name':'RFDETRKeypointPreview','outputs':{'dets':[100,4],'labels':[100,2],'keypoints':[100,34,8]},
             'source_onnx':'66ebb0386fefc400d4bd89fe74cd840d526f179b13ca73cd73850993ceb4aba5'},
    'fire': {'name':'RFDETRFireSmoke','outputs':{'dets':[300,4],'labels':[300,3]},
             'source_onnx':'56b509d8d7163c8e8a3d41eab76d81fb62bb8a16ac204be2db5619ee7c362482'},
    'crowd': {'name':'Awiros_person_and_head_detection','outputs':{'dets':[300,4],'labels':[300,3]},
              'source_onnx':'98f5c29c60c4bb60120e26e0287bea456c1bb503b4c2d21f4b0d20d8e47868dc'},
}


def profile(name, *, instances=8, batch=4, workspace=1024):
    require(name in PROFILES,'MODEL_UNKNOWN','medium/pose/fire/crowd 중 하나를 선택하세요.')
    require(type(instances) is int and 1<=instances<=16 and type(batch) is int and 1<=batch<=8
            and type(workspace) is int and 256<=workspace<=8192,'MODEL_OPTIONS','instance1~16 / batch1~8 / workspace256~8192 MiB 범위를 사용하세요.')
    return dict(copy.deepcopy(PROFILES[name]),key=name,instances=instances,batch=batch,workspace_mib=workspace,
                input_dtype='FP16',output_dtype='FP32',engine='FP16_OPTIMIZED',resolution=512 if name=='crowd' else 576,
                queue_delay_us=5000,container_gpu=0,source_input_dtype='FP32',
                deployment_note='instance8은 기존 단독모델 시험 기준. Blackwell24GB 세모델 동시 수용을 보장하지 않음.')


def pbtxt(p, *, model_version=1, instances=None):
    require(type(model_version) is int and model_version>0,'MODEL_VERSION','양의 정수 model version이 필요합니다.')
    count=p['instances'] if instances is None else instances
    require(type(count) is int and 1<=count<=16,'MODEL_OPTIONS','instance1~16을 사용하세요.')
    out=[f'name: "{p["name"]}"','platform: "tensorrt_plan"',f'max_batch_size: {p["batch"]}',
         f'version_policy {{ specific {{ versions: [{model_version}] }} }}',
         'input [{ name: "input" data_type: TYPE_FP16 dims: [3, '+str(p['resolution'])+', '+str(p['resolution'])+'] }]', 'output [']
    # JSON receipts sort object keys; configuration order must not depend on that round trip.
    out.append(',\n'.join('{ name: "'+name+'" data_type: TYPE_FP32 dims: ['+
                          ', '.join(map(str,p['outputs'][name]))+'] }' for name in PROFILES[p['key']]['outputs']))
    out+= [']',f'instance_group [{{ kind: KIND_GPU count: {count} gpus: [0] }}]',
           f'dynamic_batching {{ preferred_batch_size: [{p["batch"]}] max_queue_delay_microseconds: 5000 }}']
    return '\n'.join(out)+'\n'


def trtexec_args(p):
    r=p['resolution']
    return ['/usr/bin/trtexec','--onnx=/input/model.onnx','--saveEngine=/output/model.plan','--fp16',
            '--inputIOFormats=fp16:chw','--outputIOFormats='+','.join('fp32:chw' for _ in p['outputs']),
            f'--minShapes=input:1x3x{r}x{r}',f'--optShapes=input:{p["batch"]}x3x{r}x{r}',
            f'--maxShapes=input:{p["batch"]}x3x{r}x{r}',f'--memPoolSize=workspace:{p["workspace_mib"]}',
            '--skipInference','--profilingVerbosity=detailed']


def _helper_args(image, name, mounts, gpu=None):
    args=['run','--name',name,'--pull','never','--network','none','--init','--cpus','4',
          '--memory','12g','--pids-limit','128','--cap-drop','ALL',
          '--security-opt','no-new-privileges','--label','contextra.deployment.helper=true','--label','contextra.deployment.owner='+name]
    if gpu is not None: args+=['--gpus','device='+str(gpu)]
    for src,dest,readonly in mounts:
        require(',' not in str(src),'MOUNT_PATH','모델 도구 경로에 쉼표를 사용하지 마세요.')
        args+=['--mount',f'type=bind,source={src},target={dest}'+(',readonly' if readonly else '')]
    args+=['--entrypoint','python3',image]
    return args


def _once(image, mounts, command, *, gpu=None, timeout=900, evidence=None):
    name='contextra-model-'+uuid.uuid4().hex[:12]
    args=_helper_args(image,name,mounts,gpu)
    args[0]='create'; args[-2]=command[0]
    cid=None
    try:
        _,raw=docker(*args,*command[1:],timeout=30)
        cid=raw.strip().splitlines()[-1]
        require(re.fullmatch(r'[0-9a-f]{64}',cid),'HELPER_ID','생성한 helper ID를 확인하지 못했습니다.')
        _,raw=docker('start','--attach',cid,timeout=timeout,limit=32*1024**2)
        _,state=docker('inspect','--format','{{json .State}}',cid)
        require(json.loads(state).get('ExitCode')==0,'MODEL_HELPER_FAILED','모델 helper가 실패했습니다. 임의 재시도하지 않습니다.')
        return raw
    finally:
        try:
            if cid and evidence is not None:
                _,logs=docker('logs','--tail','2000',cid,check=False,timeout=10,
                              limit=4*1024**2,merge_output=True)
                atomic_write(evidence,logs,mode=0o600)
        finally:
            cleanup_helper(name,cid)


def serialized_gpu(function):
    from functools import wraps
    @wraps(function)
    def guarded(*args,**kwargs):
        if not kwargs.get('apply',False): return function(*args,**kwargs)
        gpu=kwargs.get('gpu',0)
        require(type(gpu) is int and 0<=gpu<=15,'GPU_SELECTION','GPU index를 확인하세요.')
        with lock(Path('/run/lock')/('contextra-model-gpu-'+str(gpu))):
            _,raw=run(['nvidia-smi',f'--id={gpu}','--query-compute-apps=pid','--format=csv,noheader,nounits'])
            require(not raw.strip(),'GPU_BUSY','선택한 GPU에서 다른 계산이 진행 중입니다. 서비스는 자동 정지하지 않았습니다.')
            return function(*args,**kwargs)
    return guarded


@serialized_gpu
def build(name, onnx_path, output, image, *, gpu=0, apply=False, instances=8, batch=4, workspace=1024,
          allow_new_onnx=False):
    p=profile(name,instances=instances,batch=batch,workspace=workspace)
    src,out=absolute(onnx_path,no_symlinks=True),absolute(output,no_symlinks=True)
    require(src.is_file() and not out.exists(),'MODEL_INPUT_OUTPUT','ONNX 파일과 아직 없는 새 output 폴더를 지정하세요.')
    require(type(gpu) is int and 0<=gpu<=15,'GPU_SELECTION','host GPU index0~15를 선택하세요.')
    source_hash=sha256(src)
    require(allow_new_onnx or source_hash==p['source_onnx'],'MODEL_LINEAGE_REVIEW',
            '기존 수용 ONNX와 다릅니다. 검토 후 --allow-new-onnx를 명시하세요. 정확도 수용은 별도입니다.')
    plan=dict(profile=p,onnx_sha256=source_hash,output=str(out),build=trtexec_args(p),
              network='none',gpu=gpu,image=image,live_repository_change=False,
              required_in_image=['onnx','TensorRT10.16 Python OR approved C++ probe','numpy','tritonclient.grpc','/usr/bin/trtexec'])
    if not apply: return plan
    iid=image_id(image)
    require(image.startswith('sha256:') and iid==image,'PINNED_BUILDER','미리 준비한 로컬 builder image의 sha256 ID를 사용하세요.')
    nearest=out.parent
    while not nearest.exists(): nearest=nearest.parent
    require(shutil.disk_usage(nearest).free>=15*1024**3,'MODEL_DISK','변환 staging에 최소15GiB 여유가 필요합니다.')
    out.mkdir(parents=True,mode=0o700)
    probe=Path(__file__).with_name('model_probe.py').resolve()
    write_json(out/'profile.json',p,new=True)
    write_json(out/'manifest.json',dict(status='BUILD_INTENT',profile=p,onnx_sha256=source_hash,image_id=iid,time=now()),new=True)
    mounts=[(src,'/input/model.onnx',True),(out,'/output',False),(probe,'/kit/model_probe.py',True)]
    base=['python3','/kit/model_probe.py']
    try:
        raw=_once(iid,mounts,[*base,'onnx','--path','/input/model.onnx','--profile','/output/profile.json'],timeout=60)
        onnx_check=json.loads(raw.strip().splitlines()[-1])
        _,gpu_raw=run(['nvidia-smi',f'--id={gpu}','--query-gpu=name,uuid,driver_version','--format=csv,noheader'])
        build_log=_once(iid,mounts,trtexec_args(p),gpu=gpu,evidence=out/'build-container.log')
        atomic_write(out/'build.log',build_log,mode=0o600)
        raw=_once(iid,mounts,[*base,'engine','--path','/output/model.plan','--profile','/output/profile.json'],gpu=gpu,timeout=60)
        engine_check=json.loads(raw.strip().splitlines()[-1])
        require(sha256(src)==source_hash,'SOURCE_CHANGED','변환 중 원본 ONNX가 바뀌었습니다.')
        atomic_write(out/'config.pbtxt',pbtxt(p),new=True,mode=0o644)
        manifest=dict(status='CONTRACT_PASS',profile=p,onnx_sha256=source_hash,engine_sha256=sha256(out/'model.plan'),
                      config_sha256=sha256(out/'config.pbtxt'),image_id=iid,gpu=gpu_raw.strip(),
                      command=trtexec_args(p),onnx=onnx_check,engine=engine_check,time=now(),
                      smoke='NOT_RUN',accuracy='NOT_RUN',runtime='NOT_RUN')
        write_json(out/'manifest.json',manifest)
        return manifest
    except Exception as exc:
        write_json(out/'failure.json',{'stage':'MODEL_BUILD','code':exc.code if isinstance(exc,KitError) else 'MODEL_BUILD_FAILED',
                                     'diagnostic':exc.diagnostic if isinstance(exc,KitError) else {}})
        atomic_write(out/'FAILED','변환 중단. 원본/부분 산출물 보존. 새 output으로 재개 전 원인 검토.\n',mode=0o600)
        raise


def candidate_manifest(directory):
    """Validate semantic identity before joining a manifest-derived path.

    A checksum alone does not prove that profile.json, config.pbtxt and the
    receipt describe the same model. Never trust a name from a mutable receipt.
    """
    d=absolute(directory,no_symlinks=True)
    m=read_json(d/'manifest.json')
    p=m.get('profile')
    require(isinstance(p,dict),'MODEL_PROFILE','후보 profile이 없습니다.')
    expected=profile(p.get('key'),instances=p.get('instances'),batch=p.get('batch'),workspace=p.get('workspace_mib'))
    require(p==expected,'MODEL_PROFILE','후보 이름/입출력/옵션이 승인 profile과 다릅니다.')
    require(read_json(d/'profile.json')==p,'MODEL_PROFILE','profile.json과 원장 profile이 다릅니다.')
    cfg=absolute(d/'config.pbtxt',no_symlinks=True)
    engine=absolute(d/'model.plan',no_symlinks=True)
    require(cfg.is_file() and cfg.stat().st_size<=65536 and engine.is_file(),
            'MODEL_INPUT_OUTPUT','후보 엔진 또는 config가 없거나 형식이 잘못됐습니다.')
    require(cfg.read_text()==pbtxt(p),'MODEL_CONFIG','config.pbtxt가 후보 profile과 일치하지 않습니다.')
    require(sha256(engine)==m.get('engine_sha256') and sha256(cfg)==m.get('config_sha256'),
            'MODEL_HASH','후보 파일이 검증 기록과 다릅니다.')
    return m


@serialized_gpu
def verify_candidate(directory,image,*,gpu=0,apply=False):
    d=absolute(directory,no_symlinks=True); m=candidate_manifest(d)
    require(m.get('status')=='CONTRACT_PASS' and sha256(d/'model.plan')==m['engine_sha256']
            and sha256(d/'config.pbtxt')==m['config_sha256'],'MODEL_NOT_VERIFIED','변환 계약/해시 검증이 선행돼야 합니다.')
    if not apply: return {'action':'ISOLATED_TRITON_SMOKE','batches':list(range(1,m['profile']['batch']+1)),
                          'instances':1,'production_instances':m['profile']['instances'],'accuracy':'NOT_RUN'}
    iid=image_id(image); require(iid==image,'PINNED_BUILDER','검증 image ID를 고정하세요.')
    name='contextra-model-smoke-'+uuid.uuid4().hex[:12]
    repo=d/'smoke-repository'; require(not repo.exists(),'SMOKE_EXISTS','이 후보는 이미 smoke를 시도했습니다. 원장부터 확인하세요.')
    model=repo/m['profile']['name']; (model/'1').mkdir(parents=True)
    os.link(d/'model.plan',model/'1/model.plan')
    atomic_write(model/'config.pbtxt',pbtxt(m['profile'],instances=1),new=True,mode=0o644)
    probe=Path(__file__).with_name('model_probe.py').resolve()
    args=_helper_args(iid,name,[(repo,'/repo',True),(d/'profile.json','/profile.json',True),(probe,'/kit/model_probe.py',True)],gpu)
    args[0]='create'; args[-2]='tritonserver'
    cid=None
    try:
        _,raw=docker(*args,'--model-repository=/repo','--model-control-mode=explicit',
                    '--load-model='+m['profile']['name'],'--strict-readiness=true','--exit-on-error=true',timeout=30)
        cid=raw.strip().splitlines()[-1]
        require(re.fullmatch(r'[0-9a-f]{64}',cid),'HELPER_ID','helper ID 확인 실패')
        docker('start',cid,timeout=30)
        for _ in range(60):
            code,_raw=docker('exec',cid,'python3','/kit/model_probe.py','ready','--profile','/profile.json',check=False,timeout=5)
            if code==0: break
            time.sleep(1)
        else: raise KitError('MODEL_NOT_READY','격리 Triton의 모델이 준비되지 않았습니다.')
        _,raw=docker('exec',cid,'python3','/kit/model_probe.py','smoke','--profile','/profile.json',timeout=60)
        result=json.loads(raw.strip().splitlines()[-1])
        m['smoke']=result; m['validation_image_id']=iid; write_json(d/'manifest.json',m)
        return result
    except Exception as exc:
        preserve_smoke_failure(d,cid,exc)
        raise
    finally:
        cleanup_helper(name,cid)


def preserve_smoke_failure(directory, cid, error):
    """Private bounded diagnostics must be attempted before helper cleanup, never block it."""
    report={'stage':'MODEL_SMOKE','code':error.code if isinstance(error,KitError) else 'MODEL_SMOKE_FAILED'}
    if cid and re.fullmatch(r'[0-9a-f]{64}',cid):
        try:
            code,logs=docker('logs','--tail','200','--timestamps',cid,
                             check=False,timeout=10,limit=1024*1024,merge_output=True)
            atomic_write(directory/'smoke-triton.log',logs,mode=0o600,new=True)
            report['log_capture_exit']=code
        except Exception:
            report['log_capture']='FAILED'
        try:
            code,raw=docker('inspect','--format','{{json .State}}',cid,check=False,timeout=10)
            if code==0:
                state=json.loads(raw)
                report['state']={k:state.get(k) for k in ('Running','ExitCode','OOMKilled','Status')}
        except Exception:
            report['state_capture']='FAILED'
    try:
        write_json(directory/'smoke-failure.json',report,new=True)
    except Exception:
        # Preserve the original exception and still reclaim the helper on a full disk.
        pass


def install_candidate(directory,destination,*,apply=False):
    d=absolute(directory,no_symlinks=True); m=candidate_manifest(d)
    require(m.get('status')=='CONTRACT_PASS' and isinstance(m.get('smoke'),dict)
            and m['smoke'].get('shape_dtype_finite')=='PASS','MODEL_NOT_VERIFIED','격리 batch smoke PASS가 필요합니다.')
    require(sha256(d/'model.plan')==m['engine_sha256'] and sha256(d/'config.pbtxt')==m['config_sha256'],
            'MODEL_HASH','후보 파일이 검증 후 바뀌었습니다.')
    target=absolute(destination,no_symlinks=True)/m['profile']['name']
    require(not target.exists(),'MODEL_EXISTS','현재 사용 중인 모델을 덮어쓰지 않습니다. 새 candidate repository를 지정하세요.')
    plan={'destination':str(target),'container_path':'/opt/contextra/models/triton/'+m['profile']['name'],
          'live_load':False,'next':'새 host 경로를 private overlay에 매핑하고 deployctl plan/승인 재배포'}
    if not apply: return plan
    require(os.geteuid()==0,'SUDO_REQUIRED','모델 배치에는 sudo가 필요합니다.')
    (target/'1').mkdir(parents=True,mode=0o750)
    shutil.copyfile(d/'model.plan',target/'1/model.plan')
    shutil.copyfile(d/'config.pbtxt',target/'config.pbtxt')
    shutil.copyfile(d/'manifest.json',target/'deployment-manifest.json')
    require(sha256(target/'1/model.plan')==m['engine_sha256'],'MODEL_COPY','복사된 엔진 해시가 다릅니다.')
    return plan


def control_mode(container):
    identifier(container)
    script="import pathlib,json; print(json.dumps([p.read_bytes().decode(errors='replace').split('\\0') for p in pathlib.Path('/proc').glob('[0-9]*/cmdline') if p.is_file() and b'tritonserver' in p.read_bytes().split(b'\\0')[0]]))"
    _,raw=docker('exec',container,'python3','-c',script,timeout=10)
    rows=json.loads(raw)
    require(len(rows)==1,'TRITON_MODE_UNKNOWN','Triton 프로세스 한 개의 실제 argv를 확인해야 합니다.')
    args=rows[0]; mode='none'
    for i,arg in enumerate(args):
        if arg.startswith('--model-control-mode='): mode=arg.split('=',1)[1]
        elif arg=='--model-control-mode' and i+1<len(args): mode=args[i+1]
    require(mode in ('none','explicit','poll'),'TRITON_MODE_UNKNOWN','알 수 없는 model-control mode입니다.')
    return {'mode':mode.upper(),'source':'RUNNING_PROCESS_ARGV; omitted uses Triton NONE default',
            'action':'명시 load 가능' if mode=='explicit' else 'NONE이면 서비스 재생성; POLL은 수동 검토. mode를 변경하지 않음'}


def load_model(container,name,*,apply=False):
    p=profile(name); mode=control_mode(container)
    require(mode['mode']=='EXPLICIT','TRITON_RESTART_REQUIRED','현재 모드에서는 API load를 사용하지 않습니다. 안전한 재배포가 필요합니다.')
    if not apply: return dict(mode,model=p['name'],mutation='SINGLE_LOAD_AFTER_APPROVAL')
    script="import urllib.request; r=urllib.request.urlopen(urllib.request.Request('http://127.0.0.1:8000/v2/repository/models/"+p['name']+"/load',data=b'{}',headers={'Content-Type':'application/json'},method='POST'),timeout=30); print(r.status)"
    docker('exec',container,'python3','-c',script,timeout=35)
    ready="import urllib.request; r=urllib.request.urlopen('http://127.0.0.1:8000/v2/models/"+p['name']+"/ready',timeout=3); print(r.status); r.close()"
    docker('exec',container,'python3','-c',ready,timeout=5)
    return {'model':p['name'],'load':'REQUEST_RETURNED','ready':'PASS','performance':'NOT_RUN'}


def export_weight(name,weight,output,image,adapter,adapter_hash,*,apply=False):
    """An explicit reviewed adapter seam; no guessed RF-DETR/Pose class fallback."""
    p=profile(name); src=absolute(weight,no_symlinks=True); out=absolute(output,no_symlinks=True)
    script=absolute(adapter,no_symlinks=True)
    require(src.is_file() and script.is_file() and sha256(script)==adapter_hash,
            'EXPORT_ADAPTER','검토된 모델별 adapter script와 정확한 checksum이 필요합니다.')
    require(not out.exists(),'FILE_EXISTS','새 export output 폴더를 사용하세요.')
    plan={'model':name,'weight_sha256':sha256(src),'adapter_sha256':adapter_hash,'image':image,
          'interface':'python3 adapter.py --weights /input/weights --output /output/model.onnx --model medium|pose|fire',
          'network':'none','automatic_download':False,'runtime':'NOT_RUN'}
    if not apply:return plan
    require(image_id(image)==image and image.startswith('sha256:'),'PINNED_BUILDER','고정된 로컬 exporter image ID를 사용하세요.')
    out.mkdir(parents=True,mode=0o700)
    raw=_once(image,[(src,'/input/weights',True),(out,'/output',False),(script,'/kit/adapter.py',True)],
              ['python3','/kit/adapter.py','--weights','/input/weights','--output','/output/model.onnx','--model',name],timeout=900)
    atomic_write(out/'export.log',raw,mode=0o600)
    require((out/'model.onnx').is_file(),'EXPORT_OUTPUT','adapter가 model.onnx를 만들지 않았습니다.')
    plan.update(onnx_sha256=sha256(out/'model.onnx'),status='EXPORTED_NOT_VALIDATED')
    write_json(out/'export-manifest.json',plan,new=True)
    return plan


def prepare_all(mapping,output,image,*,gpu=0,apply=False):
    inputs=read_json(absolute(mapping,no_symlinks=True))
    names=('medium','pose','fire')
    require(set(inputs)==set(names),'MODEL_INPUT_MAP','medium/pose/fire 각각의 ONNX 경로를 지정하세요.')
    root=absolute(output,no_symlinks=True)
    plans={n:build(n,inputs[n],root/n,image,gpu=gpu) for n in names}
    if not apply:return {'sequence':list(names),'plans':plans,'parallel_build':False,'production_load':False}
    require(not root.exists(),'FILE_EXISTS','전체 변환 작업에는 새 폴더를 사용하세요.')
    root.mkdir(parents=True,mode=0o700)
    results={}
    for n in names:
        build(n,inputs[n],root/n,image,gpu=gpu,apply=True)
        results[n]=verify_candidate(root/n,image,gpu=gpu,apply=True)
    return {'results':results,'production_load':False}


def cleanup_helper(name, cid=None):
    """Reconcile an ambiguous create response by exact unique name + ownership label.

    Never remove by prefix or list every container. A failed cleanup remains an
    explicit blocker; stopping the Docker CLI is not stopping its container.
    """
    selector=cid if cid and re.fullmatch(r'[0-9a-f]{64}',cid) else name
    code,raw=docker('inspect','--format','{{json .}}',selector,check=False,timeout=10)
    if code!=0:
        # Only a confirmed not-found for this exact owned name is an absent helper.
        # A daemon/network failure is not evidence that create did not succeed.
        missing = ('No such object: '+selector in raw or 'No such container: '+selector in raw)
        require(missing,'HELPER_CLEANUP_UNKNOWN','helper 존재 여부를 확인하지 못했습니다. 정확한 이름/ID로 operator가 확인하세요.')
        return
    d=json.loads(raw)
    require(d.get('Name')=='/'+name and d.get('Config',{}).get('Labels',{}).get('contextra.deployment.owner')==name,
            'HELPER_OWNERSHIP','helper 소유권이 다릅니다. 컨테이너를 제거하지 않았습니다.')
    exact=d.get('Id','')
    require(re.fullmatch(r'[0-9a-f]{64}',exact),'HELPER_ID','helper ID가 잘못됐습니다.')
    docker('rm','-f',exact,timeout=15)


def gpu_smoke(image, *, gpu=0, apply=False):
    require(type(gpu) is int and 0<=gpu<=15,'GPU_SELECTION','GPU index0~15를 선택하세요.')
    if not apply:return {'action':'GPU_CONTAINER_SMOKE','image':image,'gpu':gpu,'network':'none',
                         'changes':'자기 소유 helper만 생성/회수. 실제 서비스 변경 없음'}
    require(image.startswith('sha256:') and image_id(image)==image,'PINNED_BUILDER','로컬 GPU image의 immutable ID를 지정하세요.')
    result=_once(image,[],['nvidia-smi','-L'],gpu=gpu,timeout=30)
    require('GPU 0:' in result,'GPU_QUERY','container GPU를 확인하지 못했습니다.')
    return {'gpu_container':'PASS','inference':'NOT_RUN','image':image}
