- Add module-level guards for pytesseract and PIL.Image (enables patching in tests)
- Move `import io` from inside _ocr_page to module-level stdlib imports
- Extract _ensure_pil_image() helper with TypeError guard so isinstance check
does not blow up when Image is patched to a MagicMock in tests
- Add 3 new tests: pdfplumber=None ImportError, sparse-page OCR fallback,
OCR render failure returns empty chunk
- Coverage: 96% (up from 64%)
- Set points.flags.writeable = False in HandsDetector.detect() so in-place
mutation of HandLandmarks.points raises ValueError (frozen=True alone does not
protect numpy array contents)
- Extend test_handlandmarks_is_immutable to assert ValueError on array mutation
- Add test_camera.py with 3 tests covering is_open, frames() yield/break
behaviour, and context manager release (was at 0% coverage)
- Remove unused `import numpy as np` from camera.py; fix frames() return
annotation to Iterator (np.ndarray ref removed with the import)
Five backends: BGE (FlagEmbedding), Qwen3 (generative yes/no logit scorer,
batched forward pass), CrossEncoder (sentence-transformers, covers mxbai-rerank
/ ms-marco / jina), Cohere (BYOK cloud), Remote (HTTP delegate to cf-reranker
service). Mock adapter for tests. 54 tests.
cf-reranker FastAPI service app (port 8011) — cf-orch manages as a process,
defaults to Qwen3-Reranker-0.6B.
make_reranker() auto-detects CF_ORCH_URL and routes to cf-orch cf-reranker
when set — cloud apps (Kiwi, Peregrine, Snipe) get remote Qwen3 reranking
with zero code changes. Local dev falls back to local BGE.
pyproject extras: reranker-bge, reranker-qwen3, reranker-cross-encoder,
reranker-cohere, reranker-service.
Extracted from kiwi/avocet where it was duplicated. Reads llm.yaml via
the same path LLMRouter uses — products can now import detect_byok from
cf-core instead of maintaining their own copy.
Extracts the JWT validation + Heimdall tier resolution + guest session pattern
that was duplicated across kiwi and peregrine into a single reusable module.
CloudSessionFactory is parameterized by product name. Products instantiate it
once at module level and call .dependency() to get a FastAPI-compatible Depends()
function. .require_tier(min_tier) returns a dependency factory for gated routes.
CloudUser carries:
user_id — Directus UUID, "local" (self-hosted), "local-dev" (bypass), "anon-<uuid>"
tier — free | paid | premium | ultra | local
product — which CF product this session is for
has_byok — whether user has a configured LLM backend
meta — dict for product-specific extras (household_id, license_key, etc.)
Products can pass extra_meta= to attach product-specific fields without
subclassing. The module is FastAPI-only (fastapi is a lazy import so local-mode
products that never hit cloud paths don't pay the import cost).
- backends/ollama.py: routes requests to a running Ollama instance via HTTP API
- backends/vllm.py: routes requests to vllm's OpenAI-compatible API
(/v1/chat/completions); cf-text holds no GPU memory in proxy mode
- hardware/tiers.py: register cf-musicgen in 8GB, 16GB, and 32GB VRAM tiers
- tts/app.py: use inline type comment for _backend to avoid runtime global warning
- tts/backends/base.py: minor style cleanup
- create_app: add gpu_ids param; when set, exports CUDA_VISIBLE_DEVICES=<ids>
so HuggingFace Accelerate auto-shards across all listed devices
- CLI: add --gpu-ids arg (e.g. "0,1"); overrides --gpu-id when provided
- backends/base.py: propagate gpu_ids through TextBackend.generate
so backends can be aware of the visible device set
Single-GPU deployments are unaffected — --gpu-id=0 remains the default.
Adds community subcategory tagging for corpus recipes (kiwi#118).
Any product with a recipe corpus can use this to let users tag recipes
into browse taxonomy locations that FTS missed.
- 005_recipe_tags.sql: recipe_tags (per-recipe taxonomy tag with upvote
counter) + recipe_tag_votes (dedup table; submitter self-vote at insert)
- store.py: submit_recipe_tag(), upvote_recipe_tag(), get_recipe_tag_by_id(),
list_tags_for_recipe(), get_accepted_recipe_ids_for_subcategory()
Acceptance threshold: upvotes >= 2 (submitter counts as 1, one more needed).
Tags keyed as recipe_source='corpus' for future community-recipe extension.
Names cf-text, cf-voice, cf-vision as trunk services with the cf_orch
allocation block pattern. Documents all backend types (openai_compat,
anthropic, vision_service) and the env-var auto-detection path.
Adds circuitforge_core.preferences.currency with get/set_currency_code()
and format_currency(). Priority chain: store → CURRENCY_DEFAULT env → USD.
Formatting uses babel when available; falls back to a 30-currency symbol
table with correct ISO 4217 minor-unit decimal places (0 for JPY, KRW, etc.).
Consumed by Snipe, Kiwi, Peregrine, Crossbill. Bumps to v0.13.0.
12 signal functions covering staleness, repost patterns, salary transparency,
ATS blackhole detection, and enrichment signals. All pure functions — no LLM,
no network, no I/O. trust_score = 1 - sum(triggered weights), clamped to [0,1].
confidence reflects fraction of signals with available evidence.
Salary transparency enforced for CO/CA/NY/WA/IL/MA. ATS blackhole patterns:
Lever, Greenhouse, Workday, iCIMS, Taleo.
83 tests (models, all 12 signals individually, scorer). Bumps to v0.12.0.
Add early validation in create_app(): raise ValueError with a clear
message when model_path is empty and mock=False. Prevents the cryptic
HFValidationError that surfaced when cf-orch passed an empty {model}
arg (cf-orch #46). Surfaces the real problem at the service layer
rather than deep inside the HuggingFace loader.
Migration 004 creates community_categories (platform, category_id, name,
full_path, source_product, published_at) with a unique constraint on
(platform, category_id) and a name index.
SnipeCommunityStore gains publish_categories() (upsert batch) and
fetch_categories() (ordered by name, configurable limit) to support
Snipe's community category federation.
- Add 003_seller_trust_signals.sql: dedicated table for Snipe seller trust
outcomes (confirmed scammer / confirmed legitimate). Separate from the
Kiwi recipe post schema — seller signals are a different domain.
- Add SnipeCommunityStore(SharedStore): publish_seller_signal(),
list_signals_for_seller(), scam_signal_count() methods.
- Export SnipeCommunityStore + SellerTrustSignal from community __init__.
No PII stored: only platform_seller_id (public username) + flag keys.
Implements SharedStore with get_post_by_slug, list_posts (with JSONB
filter support), insert_post, and delete_post. _cursor_to_dict handles
both real psycopg2 tuple rows and mock dict rows for clean unit tests.
Also promotes community __init__.py imports from try/except guards to
unconditional now that db.py and store.py both exist.
Adds POST /v1/chat/completions to the cf-text FastAPI service so it can
be used as an openai_compat backend in LLMRouter without any router changes.
The endpoint accepts the standard OpenAI chat request format and returns
a standard chat.completion response.
4 tests added; all 36 text tests pass.
Shared router factory for storing user thumbs-up/down + correction text
on LLM outputs. Used by Linnet initially; designed to wire into any
CF product. JSONL export endpoint feeds Avocet SFT pipeline.
Only opted_in=1 rows export (consent gate for correction text).
SiglipProcessor.from_pretrained() hits a NoneType AttributeError in
tokenization_auto.py:652 on transformers 5.2.0 (TOKENIZER_MAPPING_NAMES
returns None for 'siglip' model type). Load SiglipTokenizer and
SiglipImageProcessor separately then compose them manually to bypass the
broken auto-detection path. Falls back to from_pretrained() on older
builds. Requires sentencepiece in the runtime env.
circuitforge_core.vision.router now re-exports VisionRouter from the
standalone cf-vision repo. Existing imports unchanged; falls back to
a helpful ImportError stub if cf-vision is not installed.
Closes cf-core#36
Instead of splitting SQL on semicolons (fragile — semicolons appear inside
comments and string literals), use executescript() for correct tokenization.
On 'duplicate column name' error (caused by a prior partial run that
auto-committed some ALTER TABLE statements before crashing), strip the
already-applied ADD COLUMN statement from the script and retry. Limit
to 20 attempts to prevent infinite loops on genuinely broken SQL.
This replaces the earlier per-statement split approach which broke on
migration 004 comment text containing a semicolon inside a -- comment,
causing the remainder ('one row per...') to be treated as raw SQL.
SQLite's executescript() auto-commits each DDL statement individually.
If a migration crashes mid-run, prior ALTER TABLE statements are already
committed but the migration is never recorded as applied. On restart,
the runner re-runs the same file and hits 'duplicate column name' on
already-applied statements, breaking subsequent startups permanently.
Replace executescript() with per-statement execute() calls. 'Duplicate
column name' OperationalErrors are caught and logged as warnings so the
migration can complete and be marked as done. All other errors still
propagate normally.