Simonc-44 commited on
Commit
ca38607
·
verified ·
1 Parent(s): 4361518

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +28 -67
app.py CHANGED
@@ -1,4 +1,4 @@
1
- from flask import Flask, request, jsonify, Response
2
  from transformers import pipeline, AutoTokenizer, AutoModelForCausalLM
3
  import torch
4
  import threading
@@ -25,7 +25,7 @@ SYSTEM_PROMPT_A1 = (
25
  MODEL_ID = "CygnisAI/Cygnis-Alpha-1.7B-v0.1-Instruct"
26
 
27
  # ─────────────────────────────────────────────
28
- # HTML PAGE (served at /)
29
  # ─────────────────────────────────────────────
30
  HTML_PAGE = """<!DOCTYPE html>
31
  <html lang="fr">
@@ -48,40 +48,29 @@ HTML_PAGE = """<!DOCTYPE html>
48
  font-weight: 900;
49
  }
50
  .glow { text-shadow: 0 0 40px rgba(165, 231, 255, 0.4); }
51
-
52
- /* Pulse animation on logo */
53
  @keyframes pulse-glow {
54
  0%, 100% { filter: drop-shadow(0 0 20px rgba(34,211,238,0.4)); }
55
  50% { filter: drop-shadow(0 0 45px rgba(34,211,238,0.9)); }
56
  }
57
  .logo-pulse { animation: pulse-glow 3s ease-in-out infinite; }
58
-
59
- /* Fade-in on load */
60
  @keyframes fadeUp {
61
  from { opacity: 0; transform: translateY(24px); }
62
  to { opacity: 1; transform: translateY(0); }
63
  }
64
- .fade-up { animation: fadeUp 0.9s cubic-bezier(0.22,1,0.36,1) both; }
65
- .delay-1 { animation-delay: 0.15s; }
66
- .delay-2 { animation-delay: 0.30s; }
67
-
68
- /* Status badge */
69
  @keyframes blink {
70
  0%, 100% { opacity: 1; }
71
  50% { opacity: 0.3; }
72
  }
73
  .blink { animation: blink 1.8s ease-in-out infinite; }
74
-
75
- /* Subtle scanline overlay */
76
  body::after {
77
  content: '';
78
  position: fixed; inset: 0;
79
  background: repeating-linear-gradient(
80
- 0deg,
81
- transparent,
82
- transparent 2px,
83
- rgba(0,0,0,0.03) 2px,
84
- rgba(0,0,0,0.03) 4px
85
  );
86
  pointer-events: none;
87
  z-index: 100;
@@ -89,15 +78,9 @@ HTML_PAGE = """<!DOCTYPE html>
89
  </style>
90
  </head>
91
  <body class="text-[#a5e7ff] min-h-screen flex flex-col items-center justify-center relative overflow-hidden">
92
-
93
- <!-- Carbon texture background -->
94
  <div class="absolute inset-0 opacity-5"
95
  style="background-image:url('https://www.transparenttextures.com/patterns/carbon-fibre.png')"></div>
96
-
97
- <!-- Main content -->
98
  <div class="relative z-10 flex flex-col items-center justify-center gap-10 px-6">
99
-
100
- <!-- Logo + Title row -->
101
  <div class="flex flex-row items-center justify-center gap-12 fade-up">
102
  <div class="w-36 h-36 transition-transform duration-700 hover:scale-105">
103
  <img src="/favicon.png" alt="CygnisAI Logo"
@@ -108,15 +91,11 @@ HTML_PAGE = """<!DOCTYPE html>
108
  <span class="opacity-80">Console</span>
109
  </h1>
110
  </div>
111
-
112
- <!-- Status badge -->
113
  <div class="fade-up delay-1 flex items-center gap-2 border border-white/10 rounded-full px-5 py-2
114
  bg-white/5 backdrop-blur-sm text-sm tracking-widest uppercase text-[#a5e7ff]/70">
115
  <span class="blink w-2 h-2 rounded-full bg-emerald-400 inline-block"></span>
116
  Cygnis-Alpha · Online
117
  </div>
118
-
119
- <!-- Endpoint info card -->
120
  <div class="fade-up delay-2 border border-white/10 rounded-2xl bg-white/5 backdrop-blur-sm
121
  px-8 py-6 text-sm max-w-md w-full text-left space-y-3">
122
  <p class="text-white/40 uppercase tracking-widest text-xs mb-4">API Endpoint</p>
@@ -129,12 +108,11 @@ HTML_PAGE = """<!DOCTYPE html>
129
  </div>
130
  </div>
131
  </div>
132
-
133
  </body>
134
  </html>"""
135
 
136
  # ─────────────────────────────────────────────
137
- # MODEL — lazy-loaded in a background thread
138
  # ─────────────────────────────────────────────
139
  pipe = None
140
  model_ready = False
@@ -143,48 +121,32 @@ model_error = None
143
  def load_model():
144
  global pipe, model_ready, model_error
145
  try:
146
- print(f"[CygnisAI] Loading model {MODEL_ID} …")
147
-
148
- # Use int8 quantization when bitsandbytes is available, else float32
149
- try:
150
- import bitsandbytes # noqa: F401
151
- print("[CygnisAI] bitsandbytes found — using load_in_8bit")
152
- model = AutoModelForCausalLM.from_pretrained(
153
- MODEL_ID,
154
- load_in_8bit=True,
155
- device_map="cpu",
156
- low_cpu_mem_usage=True,
157
- )
158
- except ImportError:
159
- print("[CygnisAI] bitsandbytes not found — using float32")
160
- model = AutoModelForCausalLM.from_pretrained(
161
- MODEL_ID,
162
- torch_dtype=torch.float32,
163
- device_map="cpu",
164
- low_cpu_mem_usage=True,
165
- )
166
 
167
  tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
168
-
169
- # Set pad token to avoid warnings
170
  if tokenizer.pad_token is None:
171
  tokenizer.pad_token = tokenizer.eos_token
172
 
 
 
 
 
 
 
 
173
  pipe = pipeline(
174
  "text-generation",
175
  model=model,
176
  tokenizer=tokenizer,
177
- device_map="cpu",
178
  )
179
 
180
  model_ready = True
181
- print("[CygnisAI] ✅ Model ready.")
182
  except Exception as e:
183
  model_error = str(e)
184
- print(f"[CygnisAI] ❌ Model load failed: {e}")
185
 
186
 
187
- # Start loading in background so the server answers immediately
188
  threading.Thread(target=load_model, daemon=True).start()
189
 
190
 
@@ -192,15 +154,22 @@ threading.Thread(target=load_model, daemon=True).start()
192
  # ROUTES
193
  # ─────────────────────────────────────────────
194
 
 
 
 
 
 
 
 
 
 
195
  @app.route("/", methods=["GET"])
196
  def home():
197
- """Serves the CygnisAI Console HTML page."""
198
  return Response(HTML_PAGE, mimetype="text/html")
199
 
200
 
201
  @app.route("/health", methods=["GET"])
202
  def health():
203
- """Quick health / readiness probe."""
204
  if model_error:
205
  return jsonify({"status": "error", "detail": model_error}), 500
206
  if not model_ready:
@@ -210,13 +179,11 @@ def health():
210
 
211
  @app.route("/generate", methods=["POST"])
212
  def generate():
213
- # ── Guard: model not ready yet ──────────────────────────────────────────
214
  if model_error:
215
  return jsonify({"error": f"Model failed to load: {model_error}"}), 500
216
  if not model_ready:
217
  return jsonify({"error": "Model is still loading, please retry in a moment."}), 503
218
 
219
- # ── Parse request ────────────────────────────────────────────────────────
220
  data = request.get_json(silent=True)
221
  if not data:
222
  return jsonify({"error": "Request body must be valid JSON."}), 400
@@ -225,19 +192,16 @@ def generate():
225
  if not user_prompt:
226
  return jsonify({"error": "Field 'prompt' is required and cannot be empty."}), 400
227
 
228
- # Optional params with safe defaults
229
- max_new_tokens = min(int(data.get("max_new_tokens", 512)), 1024) # cap at 1024
230
  temperature = float(data.get("temperature", 0.7))
231
  top_p = float(data.get("top_p", 0.9))
232
 
233
- # ── Build ChatML prompt ──────────────────────────────────────────────────
234
  full_prompt = (
235
  f"<|im_start|>system\n{SYSTEM_PROMPT_A1}<|im_end|>\n"
236
  f"<|im_start|>user\n{user_prompt}<|im_end|>\n"
237
  f"<|im_start|>assistant\n"
238
  )
239
 
240
- # ── Inference ────────────────────────────────────────────────────────────
241
  try:
242
  t0 = time.time()
243
  output = pipe(
@@ -253,10 +217,7 @@ def generate():
253
  )
254
  elapsed = round(time.time() - t0, 2)
255
 
256
- # Clean response: stop at im_end token
257
  response_text = output[0]["generated_text"].split("<|im_end|>")[0].strip()
258
-
259
- # Fallback if model returns empty
260
  if not response_text:
261
  response_text = "Je suis Cygnis-Alpha. Comment puis-je vous aider ?"
262
 
 
1
+ from flask import Flask, request, jsonify, Response, send_from_directory
2
  from transformers import pipeline, AutoTokenizer, AutoModelForCausalLM
3
  import torch
4
  import threading
 
25
  MODEL_ID = "CygnisAI/Cygnis-Alpha-1.7B-v0.1-Instruct"
26
 
27
  # ─────────────────────────────────────────────
28
+ # HTML PAGE
29
  # ─────────────────────────────────────────────
30
  HTML_PAGE = """<!DOCTYPE html>
31
  <html lang="fr">
 
48
  font-weight: 900;
49
  }
50
  .glow { text-shadow: 0 0 40px rgba(165, 231, 255, 0.4); }
 
 
51
  @keyframes pulse-glow {
52
  0%, 100% { filter: drop-shadow(0 0 20px rgba(34,211,238,0.4)); }
53
  50% { filter: drop-shadow(0 0 45px rgba(34,211,238,0.9)); }
54
  }
55
  .logo-pulse { animation: pulse-glow 3s ease-in-out infinite; }
 
 
56
  @keyframes fadeUp {
57
  from { opacity: 0; transform: translateY(24px); }
58
  to { opacity: 1; transform: translateY(0); }
59
  }
60
+ .fade-up { animation: fadeUp 0.9s cubic-bezier(0.22,1,0.36,1) both; }
61
+ .delay-1 { animation-delay: 0.15s; }
62
+ .delay-2 { animation-delay: 0.30s; }
 
 
63
  @keyframes blink {
64
  0%, 100% { opacity: 1; }
65
  50% { opacity: 0.3; }
66
  }
67
  .blink { animation: blink 1.8s ease-in-out infinite; }
 
 
68
  body::after {
69
  content: '';
70
  position: fixed; inset: 0;
71
  background: repeating-linear-gradient(
72
+ 0deg, transparent, transparent 2px,
73
+ rgba(0,0,0,0.03) 2px, rgba(0,0,0,0.03) 4px
 
 
 
74
  );
75
  pointer-events: none;
76
  z-index: 100;
 
78
  </style>
79
  </head>
80
  <body class="text-[#a5e7ff] min-h-screen flex flex-col items-center justify-center relative overflow-hidden">
 
 
81
  <div class="absolute inset-0 opacity-5"
82
  style="background-image:url('https://www.transparenttextures.com/patterns/carbon-fibre.png')"></div>
 
 
83
  <div class="relative z-10 flex flex-col items-center justify-center gap-10 px-6">
 
 
84
  <div class="flex flex-row items-center justify-center gap-12 fade-up">
85
  <div class="w-36 h-36 transition-transform duration-700 hover:scale-105">
86
  <img src="/favicon.png" alt="CygnisAI Logo"
 
91
  <span class="opacity-80">Console</span>
92
  </h1>
93
  </div>
 
 
94
  <div class="fade-up delay-1 flex items-center gap-2 border border-white/10 rounded-full px-5 py-2
95
  bg-white/5 backdrop-blur-sm text-sm tracking-widest uppercase text-[#a5e7ff]/70">
96
  <span class="blink w-2 h-2 rounded-full bg-emerald-400 inline-block"></span>
97
  Cygnis-Alpha · Online
98
  </div>
 
 
99
  <div class="fade-up delay-2 border border-white/10 rounded-2xl bg-white/5 backdrop-blur-sm
100
  px-8 py-6 text-sm max-w-md w-full text-left space-y-3">
101
  <p class="text-white/40 uppercase tracking-widest text-xs mb-4">API Endpoint</p>
 
108
  </div>
109
  </div>
110
  </div>
 
111
  </body>
112
  </html>"""
113
 
114
  # ─────────────────────────────────────────────
115
+ # MODEL — lazy-loaded in background thread
116
  # ─────────────────────────────────────────────
117
  pipe = None
118
  model_ready = False
 
121
  def load_model():
122
  global pipe, model_ready, model_error
123
  try:
124
+ print(f"[CygnisAI] Loading model {MODEL_ID} ...")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
  tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
 
 
127
  if tokenizer.pad_token is None:
128
  tokenizer.pad_token = tokenizer.eos_token
129
 
130
+ model = AutoModelForCausalLM.from_pretrained(
131
+ MODEL_ID,
132
+ torch_dtype=torch.float32, # float32 = stable on CPU
133
+ low_cpu_mem_usage=True, # stream weights, avoid RAM spike
134
+ )
135
+ model.eval() # disable dropout, slightly faster inference
136
+
137
  pipe = pipeline(
138
  "text-generation",
139
  model=model,
140
  tokenizer=tokenizer,
 
141
  )
142
 
143
  model_ready = True
144
+ print("[CygnisAI] Model ready.")
145
  except Exception as e:
146
  model_error = str(e)
147
+ print(f"[CygnisAI] Model load failed: {e}")
148
 
149
 
 
150
  threading.Thread(target=load_model, daemon=True).start()
151
 
152
 
 
154
  # ROUTES
155
  # ─────────────────────────────────────────────
156
 
157
+ @app.route("/favicon.png")
158
+ def favicon():
159
+ """Serve favicon.png from the project root."""
160
+ return send_from_directory(
161
+ os.path.dirname(os.path.abspath(__file__)),
162
+ "favicon.png",
163
+ mimetype="image/png"
164
+ )
165
+
166
  @app.route("/", methods=["GET"])
167
  def home():
 
168
  return Response(HTML_PAGE, mimetype="text/html")
169
 
170
 
171
  @app.route("/health", methods=["GET"])
172
  def health():
 
173
  if model_error:
174
  return jsonify({"status": "error", "detail": model_error}), 500
175
  if not model_ready:
 
179
 
180
  @app.route("/generate", methods=["POST"])
181
  def generate():
 
182
  if model_error:
183
  return jsonify({"error": f"Model failed to load: {model_error}"}), 500
184
  if not model_ready:
185
  return jsonify({"error": "Model is still loading, please retry in a moment."}), 503
186
 
 
187
  data = request.get_json(silent=True)
188
  if not data:
189
  return jsonify({"error": "Request body must be valid JSON."}), 400
 
192
  if not user_prompt:
193
  return jsonify({"error": "Field 'prompt' is required and cannot be empty."}), 400
194
 
195
+ max_new_tokens = min(int(data.get("max_new_tokens", 512)), 1024)
 
196
  temperature = float(data.get("temperature", 0.7))
197
  top_p = float(data.get("top_p", 0.9))
198
 
 
199
  full_prompt = (
200
  f"<|im_start|>system\n{SYSTEM_PROMPT_A1}<|im_end|>\n"
201
  f"<|im_start|>user\n{user_prompt}<|im_end|>\n"
202
  f"<|im_start|>assistant\n"
203
  )
204
 
 
205
  try:
206
  t0 = time.time()
207
  output = pipe(
 
217
  )
218
  elapsed = round(time.time() - t0, 2)
219
 
 
220
  response_text = output[0]["generated_text"].split("<|im_end|>")[0].strip()
 
 
221
  if not response_text:
222
  response_text = "Je suis Cygnis-Alpha. Comment puis-je vous aider ?"
223