Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion backend/.env.local
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
GEMINI_API_KEY="XYZ"
PORT=8000
DATABASE_URI="mongodb://<username>:<password>@127.0.0.1:27017/tz-fabric?authSource=admin&retryWrites=true&w=majority"
DATABASE_URI="mongodb://<username>:<password>@127.0.0.1:27017/tz-fabric?authSource=admin&retryWrites=true&w=majority"
GROQ_API_KEY="gsk_xxxxxxxxxxxxxxxxxxxxxxxxx"
9 changes: 3 additions & 6 deletions backend/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,8 @@ All notable changes to this repository will be documented in this file.

- Added build and deployment script

## [1.2.0] Sat, Dec 13, 2025

## [1.1.0] Fri, Dec 05, 2025
- Replace Gemini API with GROQ API

- Contact Us page Added
- 404 error handling
- NavBar CSS fixed

[Unreleased]
[Unreleased]
2 changes: 1 addition & 1 deletion backend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Note: according to your port change the port in `frontend/vite.config.ts` and `V

```sh
poetry shell
poetry run dev
poetry run tzfabric dev
```

## Lint
Expand Down
89 changes: 48 additions & 41 deletions backend/cli.py
Original file line number Diff line number Diff line change
@@ -1,50 +1,57 @@
# backend/cli.py
import argparse
import importlib.metadata
import os
from importlib.metadata import PackageNotFoundError

import click
import uvicorn
import importlib.metadata
from dotenv import load_dotenv

load_dotenv()

Port = int(os.getenv("PORT", "8000"))


# --- OLD BEHAVIOR preserved exactly ---
def app():
"""
Legacy function kept exactly as before.
Running: poetry run dev
"""
uvicorn.run("main:app", host="127.0.0.1", port=Port, reload=True)


# --- NEW CLI ENTRYPOINT ---
DEFAULT_HOST = os.getenv("HOST", "127.0.0.1")
DEFAULT_PORT = int(os.getenv("PORT", "8000"))


# ------------------------------------------------------------
# Root CLI group
# ------------------------------------------------------------
@click.group()
@click.version_option(
importlib.metadata.version("tz-fabric"),
"--version",
"-v",
prog_name="tzfabric",
message="tzfabric version: %(version)s",
)
def main():
"""
New CLI for tzfabric.
Supports:
tzfabric --version
tzfabric --serve
tzfabric (default → serve)
tzfabric command-line interface.
Use subcommands like: tzfabric serve, tzfabric dev.
"""
parser = argparse.ArgumentParser(prog="tzfabric")
parser.add_argument("--version", action="store_true", help="Show package version")
parser.add_argument("--serve", action="store_true", help="Start FastAPI server")
parser.add_argument("--host", default=os.getenv("HOST", "127.0.0.1"))
parser.add_argument("--port", type=int, default=Port)
parser.add_argument("--reload", action="store_true")
args = parser.parse_args()

if args.version:
try:
version = importlib.metadata.version("tzfabric")
except PackageNotFoundError:
version = "package-not-installed"
print(version)
return

# Default behavior: run the server
uvicorn.run("main:app", host=args.host, port=args.port, reload=args.reload)
pass


# ------------------------------------------------------------
# serve command
# ------------------------------------------------------------
@main.command()
@click.option("--host", default=DEFAULT_HOST, help="Host to bind.")
@click.option("--port", default=DEFAULT_PORT, help="Port to run on.")
@click.option("--reload", is_flag=True, help="Enable auto-reload.")
def serve(host, port, reload):
"""Start the FastAPI server."""
uvicorn.run("main:app", host=host, port=port, reload=reload)


# ------------------------------------------------------------
# dev command (legacy behavior)
# ------------------------------------------------------------
@main.command()
def dev():
"""Run the development server with reload enabled."""
uvicorn.run("main:app", host=DEFAULT_HOST, port=DEFAULT_PORT, reload=True)


# ------------------------------------------------------------
# Entry point for poetry / `tzfabric` script
# ------------------------------------------------------------
if __name__ == "__main__":
main()
3 changes: 1 addition & 2 deletions backend/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,12 @@
submit,
uploads,
validate_image,
contact
contact,
)
from tools.mcpserver import sse_app
from utils.emoji_logger import get_logger



class MyApp(FastAPI):
mongo_client: MongoClient
database: Database
Expand Down
7,049 changes: 4,001 additions & 3,048 deletions backend/poetry.lock

Large diffs are not rendered by default.

12 changes: 4 additions & 8 deletions backend/pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "tz-fabric"
version = "1.0.1"
version = "1.2.0"
description = "This is fabric analyzer backend"
authors = [{ name = "recursivezero", email = "recursivezero@outlook.com" }]
readme = "README.md"
Expand All @@ -23,8 +23,10 @@ dependencies = [
"pydantic>=2.11.7,<3.0.0",
"pydantic-settings>=2.10.1,<3.0.0",
"types-requests>=2.32.4.20250913,<3.0.0.0",
"click (>=8.3.1,<9.0.0)",
]


[project.optional-dependencies]
# ML-only extras
ml = [
Expand Down Expand Up @@ -70,20 +72,14 @@ extra = [
"modelcontextprotocol>=0.1.0,<0.2.0",
]

[virtualenvs]
in-project = true

[installer]
extras = ["extra"]

[project.urls]
homepage = "https://theadzip.com"
repository = "https://github.com/recursivezero/tz-fabric"
documentation = "https://github.com/recursivezero/tz-script/docs"

[project.scripts]
dev = "cli:app"
tz-fabric = "cli:main"
tzfabric = "cli:main"
lint = "lint:main"

[build-system]
Expand Down
35 changes: 17 additions & 18 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,31 +1,30 @@
# poetry.lock hash: 7e82157379e2e1571244ef9acc14acb8eed4946a
# poetry.lock hash: 2dc59897782db78815debdef2166bef678dc2cb4
# This file is generated by poetry-auto-export
# The SHA1 hash of the poetry.lock file is printed above
annotated-doc==0.0.3 ; python_version >= "3.11" and python_version < "3.13"
annotated-doc==0.0.4 ; python_version >= "3.11" and python_version < "3.13"
annotated-types==0.7.0 ; python_version >= "3.11" and python_version < "3.13"
anyio==4.10.0 ; python_version >= "3.11" and python_version < "3.13"
click==8.1.8 ; python_version >= "3.11" and python_version < "3.13"
anyio==4.12.0 ; python_version >= "3.11" and python_version < "3.13"
click==8.3.1 ; python_version >= "3.11" and python_version < "3.13"
colorama==0.4.6 ; python_version >= "3.11" and python_version < "3.13" and (platform_system == "Windows" or sys_platform == "win32")
dotenv==0.9.9 ; python_version >= "3.11" and python_version < "3.13"
fastapi==0.120.4 ; python_version >= "3.11" and python_version < "3.13"
h11==0.16.0 ; python_version >= "3.11" and python_version < "3.13"
httptools==0.6.4 ; python_version >= "3.11" and python_version < "3.13"
idna==3.10 ; python_version >= "3.11" and python_version < "3.13"
httptools==0.7.1 ; python_version >= "3.11" and python_version < "3.13"
idna==3.11 ; python_version >= "3.11" and python_version < "3.13"
jinja2==3.1.6 ; python_version >= "3.11" and python_version < "3.13"
markupsafe==3.0.2 ; python_version >= "3.11" and python_version < "3.13"
pydantic-core==2.33.2 ; python_version >= "3.11" and python_version < "3.13"
pydantic-settings==2.10.1 ; python_version >= "3.11" and python_version < "3.13"
pydantic==2.11.9 ; python_version >= "3.11" and python_version < "3.13"
python-dotenv==1.1.1 ; python_version >= "3.11" and python_version < "3.13"
markupsafe==3.0.3 ; python_version >= "3.11" and python_version < "3.13"
pydantic-core==2.41.5 ; python_version >= "3.11" and python_version < "3.13"
pydantic-settings==2.12.0 ; python_version >= "3.11" and python_version < "3.13"
pydantic==2.12.5 ; python_version >= "3.11" and python_version < "3.13"
python-dotenv==1.2.1 ; python_version >= "3.11" and python_version < "3.13"
python-multipart==0.0.20 ; python_version >= "3.11" and python_version < "3.13"
pyyaml==6.0.2 ; python_version >= "3.11" and python_version < "3.13"
sniffio==1.3.1 ; python_version >= "3.11" and python_version < "3.13"
starlette==0.48.0 ; python_version >= "3.11" and python_version < "3.13"
pyyaml==6.0.3 ; python_version >= "3.11" and python_version < "3.13"
starlette==0.49.3 ; python_version >= "3.11" and python_version < "3.13"
types-requests==2.32.4.20250913 ; python_version >= "3.11" and python_version < "3.13"
typing-extensions==4.15.0 ; python_version >= "3.11" and python_version < "3.13"
typing-inspection==0.4.1 ; python_version >= "3.11" and python_version < "3.13"
urllib3==2.5.0 ; python_version >= "3.11" and python_version < "3.13"
typing-inspection==0.4.2 ; python_version >= "3.11" and python_version < "3.13"
urllib3==2.3.0 ; python_version >= "3.11" and python_version < "3.13"
uvicorn==0.35.0 ; python_version >= "3.11" and python_version < "3.13"
uvloop==0.21.0 ; python_version >= "3.11" and python_version < "3.13" and sys_platform != "win32" and sys_platform != "cygwin" and platform_python_implementation != "PyPy"
watchfiles==1.1.0 ; python_version >= "3.11" and python_version < "3.13"
uvloop==0.22.1 ; python_version >= "3.11" and python_version < "3.13" and sys_platform != "win32" and sys_platform != "cygwin" and platform_python_implementation != "PyPy"
watchfiles==1.1.1 ; python_version >= "3.11" and python_version < "3.13"
websockets==15.0.1 ; python_version >= "3.11" and python_version < "3.13"
2 changes: 1 addition & 1 deletion backend/routes/contact.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class ContactForm(BaseModel):

@router.post("/contact")
async def save_contact(request: Request, form: ContactForm):
db = request.app.database # ← SAME as all other route files
db = request.app.database # ← SAME as all other route files

collection = db["contact_messages"]

Expand Down
3 changes: 1 addition & 2 deletions backend/routes/uploads.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
AUDIO_DIR.mkdir(parents=True, exist_ok=True)



@router.post("/uploads/tmp_media")
async def upload_tmp_media(
request: Request,
Expand All @@ -26,7 +25,7 @@ async def upload_tmp_media(
filename: str | None = Form(None), # <-- 👈 accept provided name
):
base_url = str(request.base_url).rstrip("/")

if not image and not audio:
raise HTTPException(
status_code=400,
Expand Down
12 changes: 6 additions & 6 deletions backend/routes/validate_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from fastapi.responses import JSONResponse
from PIL import ExifTags, Image

from utils.gemini_client import gemini_vision_check
from utils.groq_client import groq_vision_check

# Optional CV functions use opencv; install opencv-python-headless
# mypy doesn't ship stubs for cv2/numpy in many environments; declare as Optional[Any]
Expand Down Expand Up @@ -56,7 +56,7 @@

MAX_SIDE = 1024
JPEG_QUALITY = 80
GEMINI_TIMEOUT_SEC = 15
GROQ_TIMEOUT_SEC = 15

# ---------------- CACHE ----------------
_MAX_CACHE = 1024
Expand Down Expand Up @@ -130,10 +130,10 @@ def _to_b64(data: bytes) -> str:
return base64.b64encode(data).decode("ascii")


async def _gemini_check_base64(b64_str: str) -> str:
async def _groq_check_base64(b64_str: str) -> str:
loop = asyncio.get_running_loop()
return await loop.run_in_executor(
None, gemini_vision_check, b64_str, VALIDATION_PROMPT
None, groq_vision_check, b64_str, VALIDATION_PROMPT
)


Expand Down Expand Up @@ -349,7 +349,7 @@ async def validate_image(image: UploadFile = File(...)):
b64 = _to_b64(small_jpeg)
try:
response_text = await asyncio.wait_for(
_gemini_check_base64(b64), timeout=GEMINI_TIMEOUT_SEC
_groq_check_base64(b64), timeout=GROQ_TIMEOUT_SEC
)
except asyncio.TimeoutError:
print(f"[validate-image] timeout total={(time.time()-t0)*1000:.0f}ms")
Expand Down Expand Up @@ -476,7 +476,7 @@ def _to_optional_float(x: Any) -> Optional[float]:
_cache_set(img_hash, {"verdict": verdict, "reason": reason, "meta": out_meta})

print(
f"[validate-image] read={(t1-t0)*1000:.0f}ms resize={(t2-t1)*1000:.0f}ms gemini={(t3-t2)*1000:.0f}ms total={(t3-t0)*1000:.0f}ms verdict={verdict} meta_metrics={out_meta.get('metrics', {})}"
f"[validate-image] read={(t1-t0)*1000:.0f}ms resize={(t2-t1)*1000:.0f}ms groq={(t3-t2)*1000:.0f}ms total={(t3-t0)*1000:.0f}ms verdict={verdict} meta_metrics={out_meta.get('metrics', {})}"
)

return JSONResponse(
Expand Down
50 changes: 31 additions & 19 deletions backend/services/generate_response.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,47 @@
from utils.gemini_ap_initialize import gemini_initialize
from typing import Any, Dict
from utils.groq_ap_initialize import groq_initialize, MODEL

model = gemini_initialize()
client = groq_initialize()


def analyse_fabric_image(image_base64: str, prompt: str, idx: int) -> dict:
def analyse_fabric_image(image_base64: str, prompt: str, idx: int) -> Dict[str, Any]:
"""
Analyse a fabric image using Groq's vision model.
image_base64 must be raw base64 string (no data URL prefix).
"""

try:
print(f"[Thread] Prompt: {prompt[:50]}...")

response = model.generate_content(
contents=[
# Groq requires image as data URL
data_url = f"data:image/png;base64,{image_base64}"

response = client.chat.completions.create(
model=MODEL,
messages=[
{
"parts": [
{"text": prompt},
"role": "user",
"content": [
{"type": "text", "text": prompt},
{
"inline_data": {
"mime_type": "image/png",
"data": image_base64,
}
"type": "image_url",
"image_url": {"url": data_url},
},
]
],
}
]
],
max_tokens=512,
)

if response and hasattr(response, "text"):
# extract text in a safe, predictable way
if response.choices:
text = response.choices[0].message.content.strip()
print("[Thread] Response received.")
return {"id": idx, "response": response.text.strip()}
else:
print("[Thread] No text in response.")
return {"id": idx, "response": None}
return {"id": idx, "response": text}

print("[Thread] No text in response.")
return {"id": idx, "response": None}

except Exception as e:
print("Error in Gemini thread:", e)
print("Groq Vision Error:", e)
return {"id": idx, "response": None}
14 changes: 0 additions & 14 deletions backend/utils/gemini_ap_initialize.py

This file was deleted.

Loading