Spaces:
Running on Zero
Running on Zero
File size: 7,921 Bytes
4a4df15 3035446 4a4df15 | 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 | """Interactive installer runs INSIDE Docker; no host Python/venv required."""
from __future__ import annotations
import argparse
import getpass
from pathlib import Path
from .configuration import DATA, SECRETS, LocalConfig, DEFAULT_SPACE, load_config, save_config, repo_id
from .storage import LocalStore
def ask(text: str, default: str = '') -> str:
value=input(f'{text}' + (f' [{default}]' if default else '') + ': ').strip()
return value or default
def yes(text: str, default: bool = False) -> bool:
while True:
v=ask(text+' (y/n)','y' if default else 'n').lower()
if v in ('y','yes'): return True
if v in ('n','no'): return False
print('Enter y or n.')
def password() -> str:
while True:
p=getpass.getpass('Local password (12+ characters, hidden): ')
q=getpass.getpass('Repeat password: ')
if p==q and 12<=len(p)<=512:
return p
print('Passwords must match and contain 12-512 characters.')
def new_user(store, *, admin=False):
while True:
username=ask('Local username').lower()
try:
uid=store.create_user(username,password(),admin=admin)
print(f'Created local account: {username}')
return uid
except ValueError as exc:
print(exc)
def configure_token(store,uid):
u=store.by_id(uid)
print(f"HF settings for {u['username']} (not your local password).")
print('No token is needed for CPU mode. You may skip now and add your personal token in the app.')
token=getpass.getpass('Personal HF token (hidden; Enter to keep/skip): ').strip()
consent=yes('Allow sending source excerpts, question, rubric and transcript to the configured HF Space?',bool(u['remote_consent']))
store.preferences(uid,u['backend'],consent,token)
def manage(store,config):
# This CLI requires Docker/host access, which is already administrative.
# Still authenticate a local admin when account login is enabled.
if config.accounts:
name=ask('Local administrator username')
if not store.authenticate(name,getpass.getpass('Administrator password: ')):
raise SystemExit('Administrator sign-in failed.')
admin=store.user(name)
if not admin['admin']:
raise SystemExit('Administrator account required.')
else:
admin=store.user('local')
while True:
print('\nAccounts: '+', '.join(u['username']+(' (admin)' if u['admin'] else '') for u in store.list_users()))
op=ask('Action: add / password / token / delete / done','done')
if op=='done': return
if op=='add':
uid=new_user(store,admin=yes('Administrator account?'))
if yes('Configure this account HF token now?'): configure_token(store,uid)
continue
target=store.user(ask('Target username'))
if not target:
print('Account not found.');continue
try:
if op=='password':
store.reset_password(target['id'],password())
print('Password reset. Restart the app to invalidate existing sign-ins.')
elif op=='token': configure_token(store,target['id'])
elif op=='delete':
if yes('Permanently delete this account, saved courses and history?'):
store.delete_user(admin['id'],target['id'])
print('Account deleted. Restart the app to end its existing sessions.')
else: print('Unknown action.')
except ValueError as exc: print(exc)
def main():
parser=argparse.ArgumentParser()
parser.add_argument('--manage-users',action='store_true')
args=parser.parse_args()
store=LocalStore(DATA,SECRETS)
config=load_config() if (DATA/'settings.json').exists() else LocalConfig()
if args.manage_users:
return manage(store,config)
print('\nDOCUMENT EXAM TRAINER - LOCAL SETUP\n')
print('Data and encrypted tokens will be saved in separate named Docker volumes.')
print('The application port is restricted to localhost by default. No public access is configured.')
if (DATA/'settings.json').exists() and not yes('An installation exists. Change its settings?',False):
print('Keeping existing configuration and all saved data.');return
config.accounts=yes('Require local username/password login? (recommended for shared computers)',config.accounts)
if config.accounts:
admins=[u for u in store.list_users() if u['admin'] and u['enabled'] and u['username']!='local']
if not admins:
print('Create the first local administrator account.')
uid=new_user(store,admin=True)
else:
uid=admins[0]['id']
print('Keeping existing accounts and passwords.')
while yes('Add another local account?'):
new_user(store)
else:
print('Single-user mode has no login. Anyone who can use this computer/localhost port can access its local profile.')
u=store.user('local')
uid=u['id'] if u else store.create_user('local','',admin=True,single=True)
while True:
choice=ask('Default inference: 1 = remote HF Space, 2 = local CPU', '1' if config.default_backend=='remote_hf' else '2')
if choice in ('1','2'): break
config.default_backend='remote_hf' if choice=='1' else 'local_cpu'
while True:
try:
config.remote_space=repo_id(ask('Remote Space (used only in remote mode)',config.remote_space))
break
except ValueError as exc: print(exc)
config.allow_anonymous=yes('Allow remote requests without a personal HF token? Anonymous quotas are more restrictive.',config.allow_anonymous)
print('\nCPU models: 1 = compact Qwen3-1.7B (both tasks), 2 = Qwen3-4B (both tasks), 3 = custom public HF model IDs.')
print('CPU inference is slower and the compact model is less capable than the hosted 14B evaluator.')
print('Planning estimates: allow roughly 12-16 GB RAM for compact CPU mode, 24+ GB for 4B; actual usage varies. No quantization is enabled for the CPU LLM.')
choice=ask('CPU model preset','1')
if choice=='1': config.cpu_question_model=config.cpu_evaluation_model='Qwen/Qwen3-1.7B'
elif choice=='2': config.cpu_question_model=config.cpu_evaluation_model='Qwen/Qwen3-4B'
elif choice=='3':
config.cpu_question_model=repo_id(ask('CPU question model',config.cpu_question_model))
config.cpu_evaluation_model=repo_id(ask('CPU evaluation model',config.cpu_evaluation_model))
print('Custom checkpoints must work with Transformers/Qwen-style chat templates; not every model is compatible.')
else: raise ValueError('Unknown CPU model preset.')
config.cpu_threads=int(ask('CPU threads',str(config.cpu_threads)))
config.offline_models=yes('Use cached models only? Choose no for the first installation.',config.offline_models)
if yes('Configure the first account HF token now?',config.default_backend=='remote_hf'):
configure_token(store,uid)
if config.accounts:
others = [u for u in store.list_users() if u['id'] != uid and u['username'] != 'local' and u['enabled']]
for other in others:
if yes(f"Configure HF token/consent for {other['username']} now?"):
configure_token(store, other['id'])
save_config(config)
print('\nSetup saved. No passwords or HF tokens were written into the image or compose file.')
print('Open http://localhost:7860 after the container starts. Each user can change backend and HF token in Account & inference.')
print('Models download on first use. CPU-only mode sends no documents/answers to a remote inference service.')
if __name__=='__main__':
try: main()
except (EOFError,KeyboardInterrupt): raise SystemExit('\nSetup cancelled. Rerun the installer to finish.')
except ValueError as exc: raise SystemExit(str(exc))
|