GitHub Actions Bot commited on
Commit
eaa0562
·
1 Parent(s): d59f4dd

Deploy: c6cdf9e43825d598e4e3b553c1069ab77675c9fb

Browse files
.env DELETED
@@ -1,2 +0,0 @@
1
- SUPABASE_URL=https://muftiyckgkedprgjdbhj.supabase.co
2
- SUPABASE_KEY=sb_publishable_zlFaPiRisg0rA_-4NGG31g_8VrLFxJu
 
 
 
.env.example DELETED
@@ -1,2 +0,0 @@
1
- SUPABASE_URL=your_supabase_url_here
2
- SUPABASE_KEY=your_supabase_anon_key_here
 
 
 
README.md CHANGED
@@ -8,23 +8,42 @@ pinned: false
8
  app_port: 7860
9
  ---
10
 
11
- # Prism Enhancer - Backend
12
 
13
- This is the backend for the Prism AI Prompt Enhancer.
14
- It runs a quantized version of **Gemma 2 (2B)** completely offline on the CPU using `llama-cpp-python`.
 
15
 
16
- ## Features
17
- - **Offline Inference**: No external APIs.
18
- - **MLOps Flywheel**: Synchronizes training data to Supabase (if configured).
19
- - **FastAPI**: High-performance async API.
20
 
21
- ## API Usage
22
- POST `/api/v1/enhance`
23
 
24
- ```json
25
- {
26
- "text": "Make this better",
27
- "mode": "creative",
28
- "platform": "chatgpt"
29
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  ```
 
 
 
 
 
 
 
 
 
 
8
  app_port: 7860
9
  ---
10
 
11
+ # Prism Enhancer v3 - Multimodal Engine
12
 
13
+ This backend runs a dual-model pipeline for text and image understanding.
14
+ - **Vision Brain**: `vikhyatk/moondream2` (CPU Optimized)
15
+ - **Logic Brain**: `Qwen2.5-1.5B-Instruct` (GGUF)
16
 
17
+ ## 🛠️ Supabase SQL Setup
 
 
 
18
 
19
+ Run this in your Supabase SQL Editor:
 
20
 
21
+ ```sql
22
+ -- 1. Create Storage Bucket for User Uploads
23
+ insert into storage.buckets (id, name, public)
24
+ values ('user_uploads', 'user_uploads', true);
25
+
26
+ -- 2. Create MLOps Training Logs Table
27
+ create table training_logs (
28
+ id bigint generated by default as identity primary key,
29
+ created_at timestamp with time zone default timezone('utc'::text, now()) not null,
30
+ original_text text,
31
+ enhanced_text text,
32
+ vision_description text,
33
+ image_path text,
34
+ category text
35
+ );
36
+
37
+ -- 3. Set up Storage Policies (Allow Public Read)
38
+ create policy "Public Access" on storage.objects for select using ( bucket_id = 'user_uploads' );
39
+ create policy "Allow Upload" on storage.objects for insert with check ( bucket_id = 'user_uploads' );
40
  ```
41
+
42
+ ## 🚀 API Deployment
43
+
44
+ The backend is configured for Hugging Face Spaces (Docker). It uses `llama-cpp-python` for text logic and `transformers` for vision.
45
+
46
+ **Endpoint**: `POST /api/v1/enhance` (Multipart/FormData)
47
+ - `prompt`: Text input
48
+ - `category`: [General, 3D Logo, Cartoon Logo, Future Avatar, Video]
49
+ - `file`: Optional image upload for visual reference
app/__pycache__/main.cpython-314.pyc DELETED
Binary file (6.14 kB)
 
app/main.py CHANGED
@@ -1,99 +1,95 @@
1
  import os
2
- from fastapi import FastAPI, HTTPException, Request
 
 
3
  from pydantic import BaseModel
4
  from dotenv import load_dotenv
5
- from supabase import create_client, Client
6
 
7
- from app.services.model_service import ModelService
 
 
 
8
 
9
- # Load environment variables
10
  load_dotenv()
11
 
12
- # Supabase Configuration
13
- SUPABASE_URL = os.getenv("SUPABASE_URL")
14
- SUPABASE_KEY = os.getenv("SUPABASE_KEY")
 
 
 
 
 
 
15
 
16
- # Initialize Supabase Client
17
- supabase: Client = None
18
- if SUPABASE_URL and SUPABASE_KEY:
19
- supabase = create_client(SUPABASE_URL, SUPABASE_KEY)
20
- else:
21
- print("WARNING: Supabase URL/KEY not found. Logging will be disabled.")
22
-
23
- app = FastAPI(title="Prism v2 - Offline AI & MLOps", version="2.0.0")
24
-
25
- # Data Models
26
- class EnhanceRequest(BaseModel):
27
- text: str
28
- mode: str
29
- platform: str # chatgpt | midjourney
30
 
 
31
  class EnhanceResponse(BaseModel):
32
- original_text: str
33
  enhanced_prompt: str
34
- mode: str
35
- platform: str
36
- log_id: int | None = None
37
 
38
- class FeedbackRequest(BaseModel):
39
- log_id: int
40
- liked: bool
41
-
42
- # Helper for logging interaction to Supabase and getting the ID
43
- def log_interaction(original_text: str, enhanced_text: str, mode: str, platform: str) -> int | None:
44
- if not supabase:
45
- return None
46
  try:
47
  data = {
48
- "original_text": original_text,
49
- "enhanced_text": enhanced_text,
50
- "mode": mode,
51
- "platform": platform,
52
- "liked": False
53
  }
54
- # Execute synchronously to retrieve the ID for the feedback loop
55
- res = supabase.table("training_logs").insert(data).execute()
56
- if res.data and len(res.data) > 0:
57
- return res.data[0]['id']
58
  except Exception as e:
59
- print(f"Log Error: {e}")
60
- return None
61
 
62
- @app.on_event("startup")
63
- def startup_event():
64
- # Pre-load model
65
- ModelService.get_model()
 
 
 
 
 
 
66
 
67
- @app.post("/api/v1/enhance", response_model=EnhanceResponse)
68
- def enhance_prompt(request: Request, body: EnhanceRequest):
69
- # 1. Inference
70
- enhanced_text = ModelService.enhance(body.text, body.mode, body.platform)
71
-
72
- # 2. Logging
73
- # Performed synchronously to capture the ID required for the client-side feedback loop.
74
- log_id = log_interaction(body.text, enhanced_text, body.mode, body.platform)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
- return EnhanceResponse(
77
- original_text=body.text,
78
- enhanced_prompt=enhanced_text,
79
- mode=body.mode,
80
- platform=body.platform,
81
- log_id=log_id
82
  )
83
 
84
- @app.post("/api/v1/feedback")
85
- def feedback_loop(body: FeedbackRequest):
86
- if not supabase:
87
- raise HTTPException(status_code=503, detail="Supabase not configured")
88
-
89
- try:
90
- # Update the log entry
91
- supabase.table("training_logs").update({"liked": body.liked}).eq("id", body.log_id).execute()
92
- return {"status": "success", "message": "Feedback recorded"}
93
- except Exception as e:
94
- raise HTTPException(status_code=500, detail=str(e))
95
 
96
  @app.get("/")
97
- def health_check():
98
- return {"status": "running", "model": ModelService.MODEL_FILE}
99
-
 
1
  import os
2
+ import io
3
+ from contextlib import asynccontextmanager
4
+ from fastapi import FastAPI, HTTPException, UploadFile, File, Form, BackgroundTasks
5
  from pydantic import BaseModel
6
  from dotenv import load_dotenv
 
7
 
8
+ from app.services.vision_service import get_vision_service
9
+ from app.services.storage_service import storage_service
10
+ from app.services.llm_service import llm_service
11
+ from app.services.prompt_logic import prompt_logic
12
 
 
13
  load_dotenv()
14
 
15
+ # Lifecycle Manager
16
+ @asynccontextmanager
17
+ async def lifespan(app: FastAPI):
18
+ # Pre-load models once at startup
19
+ print("Initializing Dual-Brain Engine...")
20
+ get_vision_service()
21
+ llm_service.get_model()
22
+ yield
23
+ print("Shutting down Dual-Brain Engine...")
24
 
25
+ app = FastAPI(title="Prism v3 - Multimodal AI Engine", version="3.0.0", lifespan=lifespan)
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
+ # Response Model
28
  class EnhanceResponse(BaseModel):
 
29
  enhanced_prompt: str
30
+ vision_analysis: str | None = None
31
+ image_url: str | None = None
 
32
 
33
+ def log_to_supabase(original, enhanced, vision_desc, img_url, category):
 
 
 
 
 
 
 
34
  try:
35
  data = {
36
+ "original_text": original,
37
+ "enhanced_text": enhanced,
38
+ "vision_description": vision_desc,
39
+ "image_path": img_url,
40
+ "category": category
41
  }
42
+ storage_service.supabase.table("training_logs").insert(data).execute()
 
 
 
43
  except Exception as e:
44
+ print(f"MLOps Log Error: {e}")
 
45
 
46
+ @app.post("/api/v1/enhance")
47
+ async def enhance_pipeline(
48
+ background_tasks: BackgroundTasks,
49
+ prompt: str = Form(...),
50
+ category: str = Form("general"),
51
+ file: UploadFile | None = File(None)
52
+ ):
53
+ vision_desc = None
54
+ img_url = None
55
+ opt_bytes = None
56
 
57
+ # Step 1 & 2: Process Image if exists
58
+ if file:
59
+ file_bytes = await file.read()
60
+
61
+ # 1. Optimize
62
+ opt_bytes = storage_service.optimize_image(file_bytes)
63
+
64
+ # 2. Analyze (Vision Brain)
65
+ vision_desc = get_vision_service().analyze_image(opt_bytes)
66
+
67
+ # 3. Upload (Vault) - Background Task to keep API fast
68
+ # Note: We actually need the URL for the response if possible,
69
+ # but let's upload and return the path/url as requested.
70
+ img_url = storage_service.upload_image(opt_bytes)
71
+
72
+ # Step 3: Enhance (Logic Brain)
73
+ # Construct combined instruction
74
+ master_prompt = prompt_logic.construct_master_prompt(
75
+ user_text=prompt,
76
+ vision_desc=vision_desc,
77
+ category=category
78
+ )
79
 
80
+ final_prompt = llm_service.generate(master_prompt)
81
+
82
+ # Step 4: Data Flywheel Logging
83
+ background_tasks.add_task(
84
+ log_to_supabase, prompt, final_prompt, vision_desc, img_url, category
 
85
  )
86
 
87
+ return EnhanceResponse(
88
+ enhanced_prompt=final_prompt,
89
+ vision_analysis=vision_desc,
90
+ image_url=img_url
91
+ )
 
 
 
 
 
 
92
 
93
  @app.get("/")
94
+ def health():
95
+ return {"status": "running", "engine": "Prism-v3-Multimodal"}
 
app/services/__pycache__/model_service.cpython-314.pyc DELETED
Binary file (3.18 kB)
 
app/services/llm_service.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from huggingface_hub import hf_hub_download
3
+ from llama_cpp import Llama
4
+
5
+ class LLMService:
6
+ _llm = None
7
+
8
+ # Qwen2.5-1.5B is great for CPU and logic
9
+ MODEL_REPO = "bartowski/Qwen2.5-1.5B-Instruct-GGUF"
10
+ MODEL_FILE = "Qwen2.5-1.5B-Instruct-Q4_K_M.gguf"
11
+
12
+ @classmethod
13
+ def get_model(cls):
14
+ if cls._llm is None:
15
+ print(f"Loading LLM: {cls.MODEL_REPO}...")
16
+ try:
17
+ model_path = hf_hub_download(repo_id=cls.MODEL_REPO, filename=cls.MODEL_FILE)
18
+ cls._llm = Llama(
19
+ model_path=model_path,
20
+ n_ctx=4096,
21
+ verbose=False
22
+ )
23
+ print("LLM loaded successfully.")
24
+ except Exception as e:
25
+ print(f"Error loading LLM: {e}")
26
+ raise e
27
+ return cls._llm
28
+
29
+ @classmethod
30
+ def generate(cls, master_prompt: str) -> str:
31
+ llm = cls.get_model()
32
+
33
+ # Qwen2.5 chat template
34
+ prompt = f"<|im_start|>system\nYou are a professional Prompt Architect. Refine and enhance input prompts based on category and visual evidence.<|im_end|>\n<|im_start|>user\n{master_prompt}<|im_end|>\n<|im_start|>assistant\n"
35
+
36
+ output = llm.create_completion(
37
+ prompt,
38
+ max_tokens=600,
39
+ temperature=0.7,
40
+ stop=["<|im_end|>"]
41
+ )
42
+
43
+ return output['choices'][0]['text'].strip()
44
+
45
+ # Singleton helper
46
+ llm_service = LLMService()
app/services/model_service.py DELETED
@@ -1,64 +0,0 @@
1
- import os
2
- from huggingface_hub import hf_hub_download
3
- from llama_cpp import Llama
4
-
5
- class ModelService:
6
- _instance = None
7
- _llm = None
8
-
9
- # Configuration
10
- # Using a stable quantized GGUF of Gemma 2 2B
11
- MODEL_REPO = "bartowski/gemma-2-2b-it-GGUF"
12
- MODEL_FILE = "gemma-2-2b-it-Q4_K_M.gguf"
13
-
14
- @classmethod
15
- def get_model(cls):
16
- if cls._llm is None:
17
- print(f"Loading model... {cls.MODEL_REPO}/{cls.MODEL_FILE}")
18
-
19
- # Download/Load Model
20
- try:
21
- model_path = hf_hub_download(
22
- repo_id=cls.MODEL_REPO,
23
- filename=cls.MODEL_FILE,
24
- )
25
- print(f"Model path: {model_path}")
26
-
27
- # Initialize Llama
28
- cls._llm = Llama(
29
- model_path=model_path,
30
- n_ctx=2048,
31
- verbose=False
32
- )
33
- print("Model loaded successfully.")
34
- except Exception as e:
35
- print(f"Error loading model: {e}")
36
- raise e
37
-
38
- return cls._llm
39
-
40
- @staticmethod
41
- def construct_system_prompt(mode: str, platform: str) -> str:
42
- return (
43
- f"You are an expert Prompt Engineer. Rewrite the user's input to be clear, structured, "
44
- f"and optimized for {platform}. Use a {mode} tone. "
45
- f"Return ONLY the enhanced prompt, no conversational filler."
46
- )
47
-
48
- @classmethod
49
- def enhance(cls, text: str, mode: str, platform: str) -> str:
50
- llm = cls.get_model()
51
-
52
- system_prompt = cls.construct_system_prompt(mode, platform)
53
-
54
- # Gemma 2 formatting
55
- full_prompt = f"<start_of_turn>user\nSystem: {system_prompt}\nUser Input: {text}<end_of_turn>\n<start_of_turn>model\n"
56
-
57
- output = llm.create_completion(
58
- full_prompt,
59
- max_tokens=512,
60
- stop=["<end_of_turn>"],
61
- temperature=0.7
62
- )
63
-
64
- return output['choices'][0]['text'].strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/services/prompt_logic.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class PromptLogic:
2
+ SYSTEM_INSTRUCTIONS = {
3
+ "general": "Enhance the user's prompt to be more descriptive and professional while maintaining the core intent.",
4
+ "3d logo": "Professional 3D logo design, high resolution, isometric view, Octane Render, Unreal Engine 5, minimalist, clean lines, professional color palette.",
5
+ "cartoon logo": "Playful cartoon logo, vibrant colors, vector style, bold outlines, 2D minimalist, sticker aesthetic.",
6
+ "future avatar": "Cyberpunk aesthetic, neon lighting, hyper-realistic, 8k resolution, ray tracing textures, metallic and glowing accents, futuristic clothing.",
7
+ "video": "Cinematic video prompt, 4k ultra HD, 60fps, professional lighting, dolly zoom effect, anamorphic lens, ISO 400, golden hour lighting, hyper-detailed textures.",
8
+ }
9
+
10
+ @staticmethod
11
+ def construct_master_prompt(user_text: str, vision_desc: str = None, category: str = "general") -> str:
12
+ category = category.lower()
13
+ instruction = PromptLogic.SYSTEM_INSTRUCTIONS.get(category, PromptLogic.SYSTEM_INSTRUCTIONS["general"])
14
+
15
+ master_prompt = f"System Instruction: {instruction}\n\n"
16
+
17
+ if vision_desc:
18
+ master_prompt += f"Visual Reference Analysis: {vision_desc}\n\n"
19
+ master_prompt += "Ensure the final prompt incorporates the visual details (face, hair, clothing, lighting) to maintain exact character consistency.\n\n"
20
+
21
+ master_prompt += f"User Input: {user_text}\n\n"
22
+ master_prompt += "Modified Output Prompt:"
23
+
24
+ return master_prompt
25
+
26
+ # Helper
27
+ prompt_logic = PromptLogic()
app/services/storage_service.py ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import io
2
+ import uuid
3
+ import os
4
+ from PIL import Image
5
+ from supabase import create_client, Client
6
+ from dotenv import load_dotenv
7
+
8
+ load_dotenv()
9
+
10
+ class StorageService:
11
+ def __init__(self):
12
+ url = os.getenv("SUPABASE_URL")
13
+ key = os.getenv("SUPABASE_KEY")
14
+ self.supabase: Client = create_client(url, key)
15
+ self.bucket_name = "user_uploads"
16
+
17
+ def optimize_image(self, file_bytes: bytes) -> bytes:
18
+ img = Image.open(io.BytesIO(file_bytes)).convert("RGB")
19
+
20
+ # Resize if > 1024px
21
+ max_size = 1024
22
+ if max(img.size) > max_size:
23
+ ratio = max_size / max(img.size)
24
+ new_size = (int(img.size[0] * ratio), int(img.size[1] * ratio))
25
+ img = img.resize(new_size, Image.LANCZOS)
26
+
27
+ # Compress and save as WebP
28
+ output = io.BytesIO()
29
+ img.save(output, format="WEBP", quality=85)
30
+ return output.getvalue()
31
+
32
+ def upload_image(self, file_bytes: bytes) -> str:
33
+ try:
34
+ filename = f"{uuid.uuid4()}.webp"
35
+
36
+ # Binary upload to Supabase Storage
37
+ self.supabase.storage.from_(self.bucket_name).upload(
38
+ path=filename,
39
+ file=file_bytes,
40
+ file_options={"content-type": "image/webp"}
41
+ )
42
+
43
+ # Get Public URL
44
+ res = self.supabase.storage.from_(self.bucket_name).get_public_url(filename)
45
+ return res
46
+ except Exception as e:
47
+ print(f"Upload Error: {e}")
48
+ return ""
49
+
50
+ # Singleton
51
+ storage_service = StorageService()
app/services/vision_service.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from transformers import AutoModelForCausalLM, AutoTokenizer
3
+ from PIL import Image
4
+ import io
5
+
6
+ class VisionService:
7
+ def __init__(self):
8
+ self.model_id = "vikhyatk/moondream2"
9
+ self.revision = "2024-08-26" # Use a stable revision
10
+ self.device = "cpu"
11
+
12
+ print(f"Loading Vision Model: {self.model_id}...")
13
+ self.tokenizer = AutoTokenizer.from_pretrained(self.model_id, revision=self.revision)
14
+ self.model = AutoModelForCausalLM.from_pretrained(
15
+ self.model_id,
16
+ trust_remote_code=True,
17
+ revision=self.revision
18
+ ).to(self.device)
19
+ self.model.eval()
20
+
21
+ def analyze_image(self, image_bytes: bytes) -> str:
22
+ try:
23
+ image = Image.open(io.BytesIO(image_bytes)).convert("RGB")
24
+ enc_image = self.model.encode_image(image)
25
+
26
+ prompt = "Describe this person's face, hair, body type, lighting, and clothing in extreme detail for a 3D character reference."
27
+
28
+ description = self.model.answer_question(enc_image, prompt, self.tokenizer)
29
+ return description
30
+ except Exception as e:
31
+ print(f"Vision Analysis Error: {e}")
32
+ return "No image description available."
33
+
34
+ # Singleton instance
35
+ vision_service = None
36
+
37
+ def get_vision_service():
38
+ global vision_service
39
+ if vision_service is None:
40
+ vision_service = VisionService()
41
+ return vision_service
requirements.txt CHANGED
@@ -1,8 +1,12 @@
 
1
  fastapi==0.109.0
2
  uvicorn==0.27.0
3
- pydantic==2.6.0
4
- python-dotenv==1.0.0
5
- supabase==2.3.0
6
- huggingface-hub==0.20.3
7
- llama-cpp-python==0.2.90
8
- python-multipart==0.0.9
 
 
 
 
1
+ --index-url https://download.pytorch.org/whl/cpu
2
  fastapi==0.109.0
3
  uvicorn==0.27.0
4
+ python-multipart
5
+ torch
6
+ transformers
7
+ einops
8
+ pillow
9
+ supabase
10
+ huggingface-hub
11
+ llama-cpp-python
12
+ python-dotenv