litert-models / conversion /publish_learning.py
unicorn who dev
Complete FireViewer Android learning SDK, source and verification records
dee7f43 verified
Raw History Blame Contribute Delete
9.88 kB
"""Publish executed trainable variants beside immutable inference families."""
import json,sys,hashlib,shutil,fcntl
from pathlib import Path
from huggingface_hub import HfApi
root=Path('/workspace/vds-litert-batch2')
api=HfApi(token=(Path('/workspace/.secrets/hf_token').read_text().strip()))
manifest=json.loads((root/'package/model_manifest.json').read_text());specs={s['key']:s for g in ('general_models','fireviewer_models') for s in manifest[g]}
upstreams=json.loads((root/'prefetch_manifest.json').read_text())['models']
upstreams.update(json.loads((root/'previous-prefetch.json').read_text()))
for s in json.loads((root/'previous-models.json').read_text()):specs[s['key']]={**s,'title':s['key']}
def sha(p):return hashlib.sha256(p.read_bytes()).hexdigest()
for key in sys.argv[1:]:
directory=root/'learning'/key;report=json.loads((directory/'training_validation.json').read_text());source=json.loads((directory/'source.json').read_text())
assert report['validation_passed'] and report['optimizer_resume_exact'] and report['restart_logits_exact']
assert sha(directory/'model.tflite')==report['sha256']
spec=specs[key];upstream=upstreams[key]
destination='fireviewer/litert-models' if key.startswith('fireviewer_') else 'Charlbi/Lite_rt_prepared_for_android_dataset_builder'
if key.startswith('fireviewer_') and not spec.get('publish_default',False):
# User explicitly requested FireViewer delivery; the pilot permits a
# private research copy, never a public release under this workflow.
assert source.get('private_pilot_only') and api.repo_info(destination).private
stage=directory/'publish';stage.mkdir(exist_ok=True)
shutil.copy2(directory/'model.tflite',stage/'model.tflite')
labels=source.get('labels') or json.loads((root/'learning/imagenet-labels.json').read_text())
labels=[label if labels.count(label)==1 else f'{label} [class {i}]' for i,label in enumerate(labels)]
prep=source.get('pretrained_cfg',{})
config={'task':'classification' if prep else 'object_detection','adapter':'classification' if prep else 'yolo','inputWidth':224,'inputHeight':224,'inputLayout':'NCHW','inputType':'FLOAT32','mean':0.,'std':1.,'resizeMode':'stretch','labels':labels,'threads':2,'outputIndex':0,'threshold':.1,'scoreActivation':'softmax' if prep else 'none','dynamicMinSize':128,'dynamicMaxSize':1280,'dynamicStride':32 if key=='convformer_s18' or not prep else 1}
if prep:
config.update(channelMean=[v*255 for v in prep['mean']],channelStd=[v*255 for v in prep['std']])
training={'targetEncoding':'one_hot','targetShape':[1,len(labels)],'inferOutputs':['scores']}
else:
config.update(inputWidth=source.get('app_size',320),inputHeight=source.get('app_size',320),mean=0.,std=255.,resizeMode='letterbox' if source['kind']=='yolo' else 'stretch',outputLayout='BCN',coordinates='pixels',outputIndexBoxes=0,outputIndexScores=1)
if source.get('channelMean'):config.update(channelMean=source['channelMean'],channelStd=source['channelStd'])
training={'targetEncoding':'boxes_xyxy_class_mask','targetShape':[1,100,6],'inferOutputs':['detections']}
training.update(trainSignature='train',inferSignature='infer',saveSignature='save',restoreSignature='restore',imageInput='x',targetInput='y',lossOutput='loss',checkpointInput='checkpoint_path',learningRateInput='learning_rate',scope=report['training_scope'])
config['training']=training
config['isRgb']=source.get('isRgb',True)
config['dynamicStride']=report.get('variable_image_preprocessing',{}).get('stride',32 if key=='convformer_s18' else 1)
config['dynamicMinSize']=source.get('dynamic_min_size',128)
if (directory/'android_model_config.json').exists():config=json.loads((directory/'android_model_config.json').read_text())
# The default graph's output order is not necessarily the named detection
# signature order. Resolve actual tensor indices for the Android adapter.
import tensorflow as tf
engine=tf.lite.Interpreter(model_path=str(directory/'model.tflite'),experimental_op_resolver_type=tf.lite.experimental.OpResolverType.BUILTIN_WITHOUT_DEFAULT_DELEGATES)
named=engine.get_signature_runner('infer').get_output_details()
main=engine.get_output_details()
output_indices={name:next(j for j,d in enumerate(main) if d['index']==details['index']) for name,details in named.items()}
config['namedOutputIndices']=output_indices
if config['adapter']=='yolo':
config['outputIndex']=output_indices['detections']
config.pop('outputIndexBoxes',None);config.pop('outputIndexScores',None)
del engine
(stage/'android_model_config.json').write_text(json.dumps(config,indent=2)+'\n')
report['runtime_requirement']='LiteRT builtin CPU; disable default XNNPACK delegates; Select TF Ops required for checkpoint Save/Restore'
report['android_runtime_dependencies']=['org.tensorflow:tensorflow-lite:2.16.1','org.tensorflow:tensorflow-lite-select-tf-ops:2.16.1']
report['frozen_backbone_sha256']=sha(Path(source['backbone']))
report['head_initialization']=source.get('head_initialization','source_model_or_identity_adapter')
builder='build_trainable_fire_multitask.py' if source.get('kind')=='multitask' else 'build_trainable_classifier.py' if prep else 'build_trainable_heatmap.py' if source.get('kind')=='heatmap' else 'build_trainable_detector.py'
report['conversion_source_sha256']={n:sha(root/n) for n in ['merge_training_graph.py',builder]}
if source.get('parity_report'):
parity=Path(source['parity_report']);shutil.copy2(parity,stage/'source_parity.json')
report['source_parity_report']='source_parity.json'
if (directory/'runtime216_validation.json').exists():
compatible=json.loads((directory/'runtime216_validation.json').read_text());assert compatible['sha256']==report['sha256']
shutil.copy2(directory/'runtime216_validation.json',stage/'runtime216_validation.json')
(stage/'training_validation.json').write_text(json.dumps(report,indent=2)+'\n')
contract={'schema':2,'key':key+'_learning','source_model':key,'task':config['task'],'upstream':spec.get('repo') or spec.get('checkpoint_url'),'upstream_revision':upstream.get('sha') or upstream.get('sha256'),'backbone_frozen':True,'training_scope':report['training_scope'],'report':report,'optimizer':'momentum SGD, momentum=0.9','checkpoint_contains':['trainable parameters','momentum buffers','global step'],'requires_select_tf_ops':True,'android_configuration':'android_model_config.json'}
(stage/'runtime_contract.json').write_text(json.dumps(contract,indent=2)+'\n')
local=Path(upstream['local_dir'])
for p in local.iterdir():
if p.is_file() and (p.name.lower().startswith(('license','notice','copying')) or p.name in ('CHECKPOINT_LICENSE.md','CHECKPOINT_RIGHTS.md','CITATION.md','COMMERCIAL_USE.md','MODEL_RIGHTS_POLICY.md','RIGHTS_AND_ATTRIBUTION.md','THIRD_PARTY_NOTICES.md')):shutil.copy2(p,stage/p.name)
if (local/'README.md').is_file():shutil.copy2(local/'README.md',stage/'UPSTREAM_README.md')
if key=='rtmdet_tiny':shutil.copy2(root/'vendor/mmyolo/LICENSE',stage/'LICENSE')
(stage/'README.md').write_text(f"# {spec['title']} — Android learning variant\n\nSource: `{spec.get('repo')}`, revision `{upstream.get('sha')}`.\n\nThis is a real mutable LiteRT model with `infer`, `train`, `save`, and `restore` signatures. Training executes on Android using the image and reviewed targets. Trainable scope: **{report['training_scope']}**. The visual backbone remains frozen. {report.get('limitation','')}\n\nThis learning variant is stored separately under `models/{key}_learning` and has its own explicit Android configuration.\n\nVariable image dimensions: `{report.get('variable_image_preprocessing', {'mode':'native_dynamic'})}`. The Android configuration chooses a bounded input canvas; the image may have any original size. Images are processed individually, so a dataset need not have homogeneous dimensions.\n\nCheckpoints include parameters, momentum and step. Validation executed actual updates, changed parameters, closed/reopened the interpreter, restored identical predictions and reproduced the next optimizer update. See `training_validation.json`. Tests use synthetic supervision to verify execution and persistence; no accuracy or forgetting benchmark is claimed. Android device execution: `{report.get('android_device_tested',False)}`.\n\nUse LiteRT CPU with XNNPACK disabled and Select TF Ops for checkpoint operations. The default label space is retained; changing class count requires rebuilding the head with the supplied conversion sources. Use only human-reviewed labels for training. Retain replay examples and validate a candidate before activating it.\n\nUpstream licenses and attributions continue to apply.\n")
(stage/'artifact_manifest.json').write_text(json.dumps({p.name:{'sha256':sha(p),'bytes':p.stat().st_size} for p in stage.iterdir() if p.is_file() and p.name!='artifact_manifest.json'},indent=2)+'\n')
with (root/'.publish.lock').open('a') as lock:
fcntl.flock(lock,fcntl.LOCK_EX);parent=api.repo_info(destination,repo_type='model').sha
commit=api.upload_folder(repo_id=destination,repo_type='model',folder_path=str(stage),path_in_repo=f'models/{key}_learning',parent_commit=parent,commit_message=f'Add on-device learning and optimizer resume: {key}')
path=f'models/{key}_learning/model.tflite';remote=api.get_paths_info(destination,[path],revision=commit.oid)[0]
assert remote.size==report['bytes'] and remote.lfs.sha256==report['sha256']
receipt={'repo':destination,'commit':commit.oid,'path':path,'sha256':report['sha256'],'bytes':report['bytes'],'remote_verified':True}
(directory/'publication_receipt.json').write_text(json.dumps(receipt,indent=2)+'\n');print(json.dumps(receipt),flush=True)