"""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))