viskav commited on
Commit
f60b4ac
·
verified ·
1 Parent(s): 00e7f55

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +350 -142
app.py CHANGED
@@ -1,185 +1,393 @@
1
- import asyncio
2
  import re
3
- from typing import Literal
 
 
4
  from fastapi import FastAPI, HTTPException
5
  from fastapi.middleware.cors import CORSMiddleware
6
- from pydantic import BaseModel
7
  from llama_cpp import Llama
8
-
9
- # ---------------- MODEL CONFIG ---------------- #
10
-
11
- # Model is in ROOT of the repository, not inside a folder
12
- MODEL_PATH = "/code/Phi-3.1-mini-4k-instruct-IQ2_M.gguf"
13
-
14
- # CPU settings for llama.cpp
15
- N_THREADS = 4
16
- N_CTX = 4096
17
- N_BATCH = 512
18
- N_GPU_LAYERS = 0
19
-
20
- # Concurrency limit
21
- MAX_CONCURRENT_REQUESTS = 6
22
-
23
- # Token to force stopping the generation
24
- END_TOKEN = "###END_OF_RESPONSE###"
25
-
26
- print("Loading model:", MODEL_PATH)
27
- llm = Llama(
28
- model_path=MODEL_PATH,
29
- n_threads=N_THREADS,
30
- n_ctx=N_CTX,
31
- n_batch=N_BATCH,
32
- n_gpu_layers=N_GPU_LAYERS,
33
- verbose=False,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  )
35
- print("Model loaded successfully.")
36
-
37
- # ---------------- FASTAPI APP ---------------- #
38
-
39
- app = FastAPI(title="FormatAI Humanizer Backend")
40
 
 
41
  app.add_middleware(
42
  CORSMiddleware,
43
- allow_origins=["*"], # Vercel frontend allowed
 
 
 
 
 
44
  allow_methods=["*"],
45
  allow_headers=["*"],
46
  )
47
 
48
- # ---------------- REQUEST MODELS ---------------- #
49
-
50
  class TransformRequest(BaseModel):
51
- text: str
52
- style: Literal["professional", "casual", "academic", "marketing"]
53
-
54
- class HumanizeRequest(BaseModel): # legacy
55
- text: str
56
 
 
 
57
 
58
- # ---------------- STYLE PROMPTS ---------------- #
 
59
 
 
60
  STYLE_PROMPTS = {
61
- "professional": (
62
- "STYLE: PROFESSIONAL\n"
63
- "Rewrite the user's text in a STRICTLY professional, corporate, formal tone. "
64
- "Use respectful and clear business language. Do NOT add explanations. "
65
- f"Output ONLY the rewritten text, then write {END_TOKEN}."
66
- ),
67
-
68
- "casual": (
69
- "STYLE: CASUAL\n"
70
- "Rewrite the user's text in a friendly, conversational, relaxed tone. "
71
- "Use contractions and natural flow. Do NOT add explanations. "
72
- f"Output ONLY the rewritten text, then write {END_TOKEN}."
73
- ),
74
-
75
- "academic": (
76
- "STYLE: ACADEMIC\n"
77
- "Rewrite the user's text in precise, formal academic language suitable for scholarly writing. "
78
- "Use objective vocabulary and clear structure. Do NOT add explanations. "
79
- f"Output ONLY the rewritten text, then write {END_TOKEN}."
80
- ),
81
-
82
- "marketing": (
83
- "STYLE: MARKETING\n"
84
- "Rewrite the user's text into persuasive, compelling marketing language. "
85
- "Use emotional hooks, strong benefits, and engaging tone. Do NOT add explanations. "
86
- f"Output ONLY the rewritten text, then write {END_TOKEN}."
87
- ),
88
- }
89
-
90
 
91
- # ---------------- HELPERS ---------------- #
 
 
 
 
 
92
 
93
- def clean_output(raw: str) -> str:
94
- """Trim unwanted tokens and cleanup."""
95
- if not raw:
96
- return ""
97
 
98
- # Remove system markers if any appear
99
- raw = re.sub(r"<\|/?(system|assistant|user|end)\|>", "", raw, flags=re.I)
100
 
101
- # Cut off at END_TOKEN
102
- if END_TOKEN in raw:
103
- raw = raw.split(END_TOKEN)[0]
104
 
105
- raw = raw.strip()
106
- raw = re.sub(r"[ \t]+", " ", raw)
107
- return raw.strip()
 
 
 
108
 
 
109
 
110
- def build_prompt(text: str, style: str) -> str:
111
- """Build prompt for the selected style."""
112
- system = STYLE_PROMPTS[style]
113
- return (
114
- f"<|system|>\n{system}\n\n"
115
- f"<|user|>\n{text}\n\n"
116
- f"<|assistant|>\n"
117
- )
118
 
 
119
 
120
- # ---------------- MODEL CALL ---------------- #
 
 
 
 
 
121
 
122
- async def call_llm(prompt: str, temperature: float = 0.25):
123
- loop = asyncio.get_event_loop()
124
 
125
- def sync_call():
126
- return llm(
127
- prompt,
128
- max_tokens=512,
129
- temperature=temperature,
130
- top_p=0.9,
131
- top_k=40,
132
- repeat_penalty=1.1,
133
- stop=[END_TOKEN],
134
- echo=False,
135
- )
136
 
137
- out = await loop.run_in_executor(None, sync_call)
138
 
139
- if "choices" in out:
140
- text = out["choices"][0].get("text", "")
141
- else:
142
- text = str(out)
 
 
143
 
144
- return clean_output(text)
145
 
 
 
146
 
147
- # ---------------- ENDPOINT: /api/transform ---------------- #
 
 
 
 
 
148
 
149
- @app.post("/api/transform")
150
- async def transform(req: TransformRequest):
151
- text = req.text.strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
152
  if not text:
153
- raise HTTPException(400, "Text cannot be empty")
154
-
155
- if req.style not in STYLE_PROMPTS:
156
- raise HTTPException(400, "Invalid style")
157
-
158
- # Marketing = more creativity
159
- temperature = 0.65 if req.style == "marketing" else 0.25
160
-
161
- prompt = build_prompt(text, req.style)
162
- transformed = await call_llm(prompt, temperature)
163
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
164
  return {
165
- "original": text,
166
- "transformed": transformed,
167
- "style": req.style
 
 
168
  }
169
 
170
-
171
- # ---------------- LEGACY ENDPOINT: /api/humanize ---------------- #
 
 
 
 
 
 
 
172
 
173
  @app.post("/api/humanize")
174
- async def humanize(req: HumanizeRequest):
175
- """Fallback endpoint — always uses casual."""
176
- prompt = build_prompt(req.text.strip(), "casual")
177
- out = await call_llm(prompt, temperature=0.4)
178
- return {"result": out}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
 
181
- # ---------------- HEALTH CHECK ---------------- #
 
 
 
 
 
 
 
 
 
 
182
 
183
- @app.get("/")
184
- def health():
185
- return {"status": "ok", "model": MODEL_PATH}
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
  import re
3
+ import asyncio
4
+ import time
5
+ from typing import Literal, Optional
6
  from fastapi import FastAPI, HTTPException
7
  from fastapi.middleware.cors import CORSMiddleware
8
+ from pydantic import BaseModel, Field
9
  from llama_cpp import Llama
10
+ from contextlib import asynccontextmanager
11
+
12
+ # ==================== CONFIGURATION ====================
13
+ # Hugging Face Spaces optimized settings
14
+ MODEL_REPO = "bartowski/Phi-3.1-mini-4k-instruct-GGUF"
15
+ MODEL_FILE = "Phi-3.1-mini-4k-instruct-IQ2_M.gguf"
16
+ MODEL_PATH = os.environ.get("MODEL_PATH", MODEL_FILE)
17
+
18
+ # CPU settings optimized for Spaces (2 CPU cores typical)
19
+ N_THREADS = int(os.environ.get("N_THREADS", "2"))
20
+ N_CTX = int(os.environ.get("N_CTX", "2048")) # Reduced for faster inference
21
+ N_BATCH = int(os.environ.get("N_BATCH", "128"))
22
+ N_GPU_LAYERS = int(os.environ.get("N_GPU_LAYERS", "0"))
23
+
24
+ MAX_INPUT_LENGTH = 1500
25
+ END_TOKEN = "<|endoftext|>"
26
+
27
+ # ==================== LIFECYCLE MANAGEMENT ====================
28
+ @asynccontextmanager
29
+ async def lifespan(app: FastAPI):
30
+ # Startup
31
+ print("🚀 Starting FormatAI Humanizer Backend on Hugging Face Space")
32
+ print(f"📊 Configuration:")
33
+ print(f" Model: {MODEL_PATH}")
34
+ print(f" Threads: {N_THREADS}")
35
+ print(f" Context: {N_CTX}")
36
+
37
+ # Load model on startup
38
+ global llm
39
+ llm = load_model()
40
+
41
+ yield
42
+
43
+ # Shutdown
44
+ print("👋 Shutting down...")
45
+ if llm:
46
+ del llm
47
+
48
+ # ==================== FASTAPI APP ====================
49
+ app = FastAPI(
50
+ title="FormatAI Humanizer API",
51
+ description="Backend API for text transformation with Phi-3.1 Mini",
52
+ version="1.0.0",
53
+ lifespan=lifespan
54
  )
 
 
 
 
 
55
 
56
+ # CORS - Allow your Vercel frontend
57
  app.add_middleware(
58
  CORSMiddleware,
59
+ allow_origins=[
60
+ "https://your-vercel-app.vercel.app", # Replace with your actual Vercel URL
61
+ "http://localhost:3000", # For local development
62
+ "http://localhost:5173", # Vite dev server
63
+ ],
64
+ allow_credentials=True,
65
  allow_methods=["*"],
66
  allow_headers=["*"],
67
  )
68
 
69
+ # ==================== DATA MODELS ====================
 
70
  class TransformRequest(BaseModel):
71
+ text: str = Field(..., min_length=1, max_length=MAX_INPUT_LENGTH)
72
+ style: Literal["professional", "casual", "academic", "marketing"] = "casual"
 
 
 
73
 
74
+ class HumanizeRequest(BaseModel):
75
+ text: str = Field(..., min_length=1, max_length=MAX_INPUT_LENGTH)
76
 
77
+ # ==================== GLOBAL MODEL ====================
78
+ llm = None
79
 
80
+ # ==================== STYLE PROMPTS ====================
81
  STYLE_PROMPTS = {
82
+ "professional": """You are a professional writing assistant. Rewrite the text below in formal, corporate business language.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
 
84
+ IMPORTANT RULES:
85
+ 1. Output ONLY the rewritten text
86
+ 2. No explanations, no notes
87
+ 3. Keep the same meaning
88
+ 4. Use formal vocabulary
89
+ 5. Proper grammar and structure
90
 
91
+ Text to rewrite: {text}
 
 
 
92
 
93
+ Rewritten (professional):""",
 
94
 
95
+ "casual": """You are a casual writing assistant. Rewrite the text below in friendly, natural, conversational English.
 
 
96
 
97
+ IMPORTANT RULES:
98
+ 1. Output ONLY the rewritten text
99
+ 2. No explanations, no notes
100
+ 3. Keep the same meaning
101
+ 4. Use contractions (I'm, don't, etc.)
102
+ 5. Sound like a real person speaking
103
 
104
+ Text to rewrite: {text}
105
 
106
+ Rewritten (casual):""",
 
 
 
 
 
 
 
107
 
108
+ "academic": """You are an academic writing assistant. Rewrite the text below in formal scholarly language.
109
 
110
+ IMPORTANT RULES:
111
+ 1. Output ONLY the rewritten text
112
+ 2. No explanations, no notes
113
+ 3. Keep the same meaning
114
+ 4. Use precise academic vocabulary
115
+ 5. Maintain formal structure
116
 
117
+ Text to rewrite: {text}
 
118
 
119
+ Rewritten (academic):""",
 
 
 
 
 
 
 
 
 
 
120
 
121
+ "marketing": """You are a marketing copywriter. Rewrite the text below into persuasive marketing language.
122
 
123
+ IMPORTANT RULES:
124
+ 1. Output ONLY the rewritten text
125
+ 2. No explanations, no notes
126
+ 3. Keep the same meaning
127
+ 4. Use emotional hooks and benefits
128
+ 5. Make it engaging and compelling
129
 
130
+ Text to rewrite: {text}
131
 
132
+ Rewritten (marketing):"""
133
+ }
134
 
135
+ STYLE_TEMPERATURES = {
136
+ "professional": 0.3,
137
+ "casual": 0.6,
138
+ "academic": 0.4,
139
+ "marketing": 0.7
140
+ }
141
 
142
+ # ==================== HELPER FUNCTIONS ====================
143
+ def load_model():
144
+ """Load the GGUF model"""
145
+ print(f"🔄 Loading model from: {MODEL_PATH}")
146
+
147
+ try:
148
+ # Check if model exists locally
149
+ if not os.path.exists(MODEL_PATH):
150
+ print("📥 Downloading model from Hugging Face Hub...")
151
+ try:
152
+ from huggingface_hub import hf_hub_download
153
+ MODEL_PATH = hf_hub_download(
154
+ repo_id=MODEL_REPO,
155
+ filename=MODEL_FILE,
156
+ local_dir=".",
157
+ token=os.environ.get("HF_TOKEN", None)
158
+ )
159
+ except ImportError:
160
+ print("⚠️ huggingface-hub not installed, using local model path")
161
+
162
+ model = Llama(
163
+ model_path=MODEL_PATH,
164
+ n_threads=N_THREADS,
165
+ n_ctx=N_CTX,
166
+ n_batch=N_BATCH,
167
+ n_gpu_layers=N_GPU_LAYERS,
168
+ verbose=False,
169
+ use_mlock=False, # Important for Spaces
170
+ )
171
+
172
+ print(f"✅ Model loaded successfully!")
173
+ return model
174
+
175
+ except Exception as e:
176
+ print(f"❌ Failed to load model: {e}")
177
+ return None
178
+
179
+ def clean_output(text: str) -> str:
180
+ """Clean model output"""
181
  if not text:
182
+ return ""
183
+
184
+ # Remove common artifacts
185
+ clean = re.sub(r'Rewritten\s*\([^)]+\):', '', text, flags=re.IGNORECASE)
186
+ clean = re.sub(r'IMPORTANT RULES:.*?(?=\n\n|\Z)', '', clean, flags=re.DOTALL)
187
+ clean = re.sub(r'You are [^\.]+\.', '', clean)
188
+
189
+ # Remove Phi-3.1 special tokens
190
+ clean = re.sub(r'<\|[^>]+\|>', '', clean)
191
+ clean = re.sub(r'\[/?[^]]+\]', '', clean)
192
+
193
+ # Clean whitespace
194
+ clean = re.sub(r'\n+', ' ', clean)
195
+ clean = re.sub(r'\s+', ' ', clean)
196
+ clean = clean.strip()
197
+
198
+ # Remove quotes if entire text is quoted
199
+ if clean.startswith('"') and clean.endswith('"'):
200
+ clean = clean[1:-1]
201
+
202
+ return clean
203
+
204
+ def format_prompt(text: str, style: str) -> str:
205
+ """Format prompt for Phi-3.1"""
206
+ system_prompt = STYLE_PROMPTS[style].format(text=text)
207
+
208
+ # Phi-3.1 chat format
209
+ prompt = f"<|system|>\n{system_prompt}\n<|end|>\n"
210
+ prompt += f"<|user|>\nPlease rewrite this text in {style} style:\n{text}\n<|end|>\n"
211
+ prompt += "<|assistant|>\n"
212
+
213
+ return prompt
214
+
215
+ async def transform_with_model(text: str, style: str) -> str:
216
+ """Transform text using the loaded model"""
217
+ global llm
218
+
219
+ if llm is None:
220
+ llm = load_model()
221
+ if llm is None:
222
+ raise HTTPException(status_code=503, detail="Model not available")
223
+
224
+ try:
225
+ # Build prompt
226
+ prompt = format_prompt(text, style)
227
+ temperature = STYLE_TEMPERATURES[style]
228
+
229
+ # Run inference
230
+ start_time = time.time()
231
+
232
+ output = llm(
233
+ prompt,
234
+ max_tokens=min(400, len(text) + 100), # Dynamic token limit
235
+ temperature=temperature,
236
+ top_p=0.9,
237
+ repeat_penalty=1.1,
238
+ stop=[END_TOKEN, "<|end|>", "\n\n"],
239
+ echo=False,
240
+ )
241
+
242
+ processing_time = time.time() - start_time
243
+
244
+ # Extract result
245
+ if "choices" in output and len(output["choices"]) > 0:
246
+ result = output["choices"][0]["text"]
247
+ else:
248
+ result = str(output)
249
+
250
+ # Clean result
251
+ cleaned = clean_output(result)
252
+
253
+ # Fallback if output is empty
254
+ if not cleaned or cleaned.isspace():
255
+ cleaned = f"[{style.capitalize()} Version]: {text}"
256
+
257
+ print(f"✅ Transformation completed in {processing_time:.2f}s")
258
+ return cleaned
259
+
260
+ except Exception as e:
261
+ print(f"❌ Model error: {e}")
262
+ raise HTTPException(status_code=500, detail=f"Model error: {str(e)}")
263
+
264
+ # ==================== API ENDPOINTS ====================
265
+ @app.get("/")
266
+ async def root():
267
+ """Health check endpoint"""
268
  return {
269
+ "status": "online",
270
+ "service": "FormatAI Humanizer",
271
+ "model": "Phi-3.1-mini-4k-instruct-GGUF",
272
+ "styles_available": list(STYLE_PROMPTS.keys()),
273
+ "max_input_length": MAX_INPUT_LENGTH
274
  }
275
 
276
+ @app.get("/health")
277
+ async def health_check():
278
+ """Detailed health check"""
279
+ return {
280
+ "status": "healthy" if llm else "model_loading",
281
+ "model_loaded": llm is not None,
282
+ "threads": N_THREADS,
283
+ "context_size": N_CTX
284
+ }
285
 
286
  @app.post("/api/humanize")
287
+ async def humanize_text(request: HumanizeRequest):
288
+ """
289
+ Legacy endpoint for backward compatibility
290
+ Uses casual style by default
291
+ """
292
+ start_time = time.time()
293
+
294
+ try:
295
+ result = await transform_with_model(request.text, "casual")
296
+
297
+ return {
298
+ "result": result,
299
+ "original": request.text,
300
+ "style": "casual",
301
+ "processing_time": time.time() - start_time
302
+ }
303
+
304
+ except HTTPException:
305
+ raise
306
+ except Exception as e:
307
+ raise HTTPException(status_code=500, detail=str(e))
308
 
309
+ @app.post("/api/transform")
310
+ async def transform_text(request: TransformRequest):
311
+ """
312
+ Main transformation endpoint
313
+ """
314
+ start_time = time.time()
315
+
316
+ # Validate input
317
+ if not request.text or not request.text.strip():
318
+ raise HTTPException(status_code=400, detail="Text cannot be empty")
319
+
320
+ if len(request.text) > MAX_INPUT_LENGTH:
321
+ raise HTTPException(
322
+ status_code=400,
323
+ detail=f"Text too long. Max {MAX_INPUT_LENGTH} characters"
324
+ )
325
+
326
+ if request.style not in STYLE_PROMPTS:
327
+ raise HTTPException(
328
+ status_code=400,
329
+ detail=f"Invalid style. Choose from: {list(STYLE_PROMPTS.keys())}"
330
+ )
331
+
332
+ try:
333
+ # Transform the text
334
+ transformed = await transform_with_model(request.text, request.style)
335
+
336
+ return {
337
+ "original": request.text,
338
+ "transformed": transformed,
339
+ "style": request.style,
340
+ "processing_time": round(time.time() - start_time, 2),
341
+ "success": True
342
+ }
343
+
344
+ except HTTPException:
345
+ raise
346
+ except Exception as e:
347
+ print(f"❌ Transformation error: {e}")
348
+ raise HTTPException(status_code=500, detail=f"Transformation failed: {str(e)}")
349
+
350
+ @app.get("/api/styles")
351
+ async def get_styles():
352
+ """Get available transformation styles"""
353
+ styles_info = {}
354
+ for style, prompt in STYLE_PROMPTS.items():
355
+ # Extract first line for description
356
+ first_line = prompt.split('\n')[0]
357
+ description = first_line.replace("You are ", "").replace(".", "")
358
+
359
+ styles_info[style] = {
360
+ "description": description,
361
+ "temperature": STYLE_TEMPERATURES[style],
362
+ "example_prompt": prompt[:100] + "..." if len(prompt) > 100 else prompt
363
+ }
364
+
365
+ return {
366
+ "available_styles": styles_info,
367
+ "default_style": "casual"
368
+ }
369
 
370
+ # Error handler
371
+ @app.exception_handler(Exception)
372
+ async def general_exception_handler(request, exc):
373
+ return JSONResponse(
374
+ status_code=500,
375
+ content={
376
+ "error": "Internal server error",
377
+ "message": str(exc),
378
+ "path": request.url.path
379
+ }
380
+ )
381
 
382
+ # ==================== MAIN ====================
383
+ if __name__ == "__main__":
384
+ import uvicorn
385
+
386
+ port = int(os.environ.get("PORT", 8000))
387
+
388
+ uvicorn.run(
389
+ "app:app",
390
+ host="0.0.0.0",
391
+ port=port,
392
+ reload=False # Disable reload for production
393
+ )