feat: setup wizard engine choice and Gemini-aware readiness

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
bradautomates 2026-09-17 13:47:35 +10:00
parent 6dcc18a859
commit afbc483356
2 changed files with 71 additions and 5 deletions

View File

@ -12,8 +12,8 @@ import sys
import tempfile
from pathlib import Path
from config import (CONFIG_DIR, CONFIG_FILE, ConfigError, DETAILS, get_config,
load_api_key, read_env_file, write_settings)
from config import (CONFIG_DIR, CONFIG_FILE, ConfigError, DETAILS, ENGINES, get_config,
load_api_key, load_gemini_key, read_env_file, resolve_engine, write_settings)
from runtime import configure_stdio, diagnostic, run_text
REQUIRED_BINARIES = ['ffmpeg', 'ffprobe', 'yt-dlp']
@ -23,6 +23,9 @@ ENV_TEMPLATE = '''# /watch configuration. No shell interpolation; last assignmen
# Native captions always come first. Optional fallback: auto|whisperx|groq|openai|none.
# auto preserves the Groq-then-OpenAI preference for existing installations.
# The first-run skill wizard sets WATCH_DETAIL and WATCH_WHISPER_BACKEND.
# WATCH_ENGINE=auto|gemini|local. auto uses Gemini when GEMINI_API_KEY is set.
# With the gemini engine, local videos are uploaded to Google for analysis.
GEMINI_API_KEY=
GROQ_API_KEY=
OPENAI_API_KEY=
'''
@ -142,12 +145,21 @@ def _status(detailed=False):
missing = _check_binaries()
_check_file_permissions(CONFIG_FILE)
cfg = get_config()
gemini_key = bool(load_gemini_key())
try:
engine = resolve_engine(cfg['engine'], gemini_key)
except ConfigError:
engine = 'gemini' # Explicitly chosen but the key is missing; gemini_key_present reports it.
binaries_required = engine == 'local'
has_key, detected = _have_api_key()
chosen = cfg['whisper_backend']
backend = detected if chosen == 'auto' else chosen
local_ready = _whisperx_status(cfg, detailed)
backend_ready = local_ready if chosen == 'whisperx' else (bool(load_api_key(backend)[1]) if backend in ('groq', 'openai') else chosen == 'none')
result = {'status': 'needs_install' if missing else 'ready', 'can_proceed': not missing,
blocked = bool(missing) and binaries_required
result = {'status': 'needs_install' if blocked else 'ready', 'can_proceed': not blocked,
'engine': engine, 'configured_engine': cfg['engine'], 'gemini_key_present': gemini_key,
'gemini_model': cfg['gemini_model'], 'binaries_required': binaries_required,
'first_run': is_first_run(), 'setup_complete': not is_first_run(),
'missing_binaries': missing, 'whisper_backend': backend, 'configured_backend': chosen,
'has_api_key': has_key, 'backend_ready': backend_ready,
@ -184,7 +196,20 @@ def cmd_json():
return 0
def cmd_install(backend=None, detail=None):
def cmd_install(backend=None, detail=None, engine=None):
if engine:
_scaffold_env()
write_settings({'WATCH_ENGINE': engine}, CONFIG_FILE)
if engine == 'gemini':
if not load_gemini_key():
print(f'[setup] Add GEMINI_API_KEY privately to {CONFIG_FILE} (free key: https://aistudio.google.com/apikey), '
'then rerun --engine gemini. Local videos will be uploaded to Google for analysis.', file=sys.stderr)
return 3
_write_setup_complete()
missing = _check_binaries()
note = (f" Optional: install {', '.join(_brew_pkg(missing))} for non-YouTube URLs and --engine local." if missing else '')
print(f'[setup] Gemini engine is ready. Configuration: {CONFIG_FILE}.{note}')
return 0
missing = _check_binaries()
if missing:
system = platform.system()
@ -295,13 +320,14 @@ def main():
mode.add_argument('--install-whisperx', action='store_true')
parser.add_argument('--backend', choices=['auto', 'whisperx', 'groq', 'openai', 'none'])
parser.add_argument('--detail', choices=sorted(DETAILS))
parser.add_argument('--engine', choices=sorted(ENGINES))
args = parser.parse_args()
try:
if args.check:
return cmd_check()
if args.json:
return cmd_json()
return cmd_install('whisperx' if args.install_whisperx else args.backend, args.detail)
return cmd_install('whisperx' if args.install_whisperx else args.backend, args.detail, args.engine)
except (ConfigError, OSError, ValueError) as exc:
print(f'[setup] {exc}', file=sys.stderr)
return 2

View File

@ -105,3 +105,43 @@ def test_none_backend_completion():
assert not setup.is_first_run()
assert config.get_config()['whisper_backend'] == 'none'
assert config.get_config()['detail'] == 'efficient'
def test_gemini_engine_is_ready_without_media_binaries(monkeypatch):
monkeypatch.setenv('GEMINI_API_KEY', 'unit-key')
monkeypatch.setattr(setup, '_check_binaries', lambda: list(setup.REQUIRED_BINARIES))
status = setup._status()
assert status['engine'] == 'gemini' and status['configured_engine'] == 'auto'
assert status['gemini_key_present'] is True and status['gemini_model'] == 'gemini-3.7-flash'
assert status['can_proceed'] is True and status['status'] == 'ready'
assert status['binaries_required'] is False
assert status['missing_binaries'] == ['ffmpeg', 'ffprobe', 'yt-dlp']
assert 'unit-key' not in json.dumps(status)
assert setup.cmd_check() == 0
def test_local_engine_still_requires_binaries(monkeypatch):
monkeypatch.setattr(setup, '_check_binaries', lambda: list(setup.REQUIRED_BINARIES))
status = setup._status()
assert status['engine'] == 'local' and status['gemini_key_present'] is False
assert status['can_proceed'] is False and status['binaries_required'] is True
assert setup.cmd_check() == 2
def test_engine_gemini_without_key_writes_choice_and_asks_for_key(monkeypatch, capsys):
monkeypatch.setattr(setup, '_check_binaries', lambda: list(setup.REQUIRED_BINARIES))
assert setup.cmd_install(engine='gemini') == 3
assert 'GEMINI_API_KEY' in capsys.readouterr().err
values = config.read_env_file(config.CONFIG_FILE)
assert values['WATCH_ENGINE'] == 'gemini' and values.get('SETUP_COMPLETE') != 'true'
def test_engine_gemini_with_key_completes_setup_and_preserves_lines(monkeypatch):
monkeypatch.setattr(setup, '_check_binaries', lambda: list(setup.REQUIRED_BINARIES))
config.CONFIG_DIR.mkdir(parents=True)
config.CONFIG_FILE.write_text('# my notes\nGEMINI_API_KEY=unit-key\nGROQ_API_KEY=keep-me\n')
assert setup.cmd_install(engine='gemini') == 0
text = config.CONFIG_FILE.read_text()
assert '# my notes' in text and 'GROQ_API_KEY=keep-me' in text
values = config.read_env_file(config.CONFIG_FILE)
assert values['WATCH_ENGINE'] == 'gemini' and values['SETUP_COMPLETE'] == 'true'