github-actions[bot] commited on
Commit
57150d0
Β·
1 Parent(s): 8b7118c

Auto-deploy from GitHub: c0251c0c13ce2d12b1caaed44a4b14140e5f6dca

Browse files
Files changed (6) hide show
  1. .gitattributes +0 -35
  2. Dockerfile +21 -0
  3. README.md +26 -5
  4. app.py +312 -0
  5. index.html +716 -0
  6. requirements.txt +6 -0
.gitattributes DELETED
@@ -1,35 +0,0 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
Dockerfile ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ WORKDIR /app
4
+
5
+ RUN apt-get update && apt-get install -y \
6
+ git \
7
+ curl \
8
+ build-essential \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ COPY requirements.txt .
12
+
13
+ RUN pip install --no-cache-dir -r requirements.txt
14
+
15
+ COPY . .
16
+
17
+ RUN mkdir -p temp_dir
18
+
19
+ EXPOSE 7860
20
+
21
+ CMD ["python", "app.py"]
README.md CHANGED
@@ -1,10 +1,31 @@
1
  ---
2
- title: TTT
3
- emoji: 😻
4
- colorFrom: purple
5
- colorTo: gray
6
  sdk: docker
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: TTT Text-to-Text Generator
3
+ emoji: πŸ€–
4
+ colorFrom: green
5
+ colorTo: blue
6
  sdk: docker
7
  pinned: false
8
+ license: mit
9
  ---
10
 
11
+ # Text-to-Text Generator
12
+
13
+ A Python-based text generation service powered by Qwen/Qwen3.5-4B with a neobrutalist web interface.
14
+
15
+ ## Features
16
+ - πŸ“ Submit text prompts via REST API or web UI
17
+ - πŸ€– Automatic generation using Qwen/Qwen3.5-4B
18
+ - πŸ’Ύ SQLite database for queue management with task IDs
19
+ - 🎨 Neobrutalist UI with real-time progress updates
20
+ - πŸ”„ Queue position and estimated wait time
21
+
22
+ ## Usage
23
+ Access the web interface at the Space URL above.
24
+
25
+ ## API Endpoints
26
+ - POST `/api/submit` - Submit text task
27
+ - GET `/api/tasks` - Get all tasks
28
+ - GET `/api/tasks/<id>` - Get specific task
29
+
30
+ ---
31
+ *Auto-deployed from GitHub*
app.py ADDED
@@ -0,0 +1,312 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, jsonify, send_from_directory
2
+ from flask_cors import CORS
3
+ import sqlite3
4
+ import os
5
+ import uuid
6
+ from datetime import datetime, timedelta
7
+ import threading
8
+ import time
9
+
10
+ app = Flask(__name__)
11
+ CORS(app)
12
+
13
+ # Worker state
14
+ worker_thread = None
15
+ worker_running = False
16
+
17
+ def init_db():
18
+ conn = sqlite3.connect('text_tasks.db')
19
+ c = conn.cursor()
20
+ c.execute('''CREATE TABLE IF NOT EXISTS text_tasks
21
+ (id TEXT PRIMARY KEY,
22
+ input_text TEXT NOT NULL,
23
+ system_prompt TEXT,
24
+ status TEXT NOT NULL,
25
+ result TEXT,
26
+ created_at TEXT NOT NULL,
27
+ processed_at TEXT,
28
+ progress INTEGER DEFAULT 0,
29
+ progress_text TEXT,
30
+ hide_from_ui INTEGER DEFAULT 0)'''
31
+ )
32
+ conn.commit()
33
+ conn.close()
34
+
35
+ def start_worker():
36
+ global worker_thread, worker_running
37
+ if not worker_running:
38
+ worker_running = True
39
+ worker_thread = threading.Thread(target=worker_loop, daemon=True)
40
+ worker_thread.start()
41
+ print("βœ… Worker thread started")
42
+
43
+ def cleanup_old_entries():
44
+ try:
45
+ conn = sqlite3.connect('text_tasks.db')
46
+ c = conn.cursor()
47
+ cutoff_date = (datetime.now() - timedelta(days=10)).isoformat()
48
+ c.execute('DELETE FROM text_tasks WHERE created_at < ?', (cutoff_date,))
49
+ deleted = c.rowcount
50
+ conn.commit()
51
+ conn.close()
52
+ if deleted > 0:
53
+ print(f"🧹 Cleanup: Deleted {deleted} old task entries")
54
+ except Exception as e:
55
+ print(f"⚠️ Cleanup error: {e}")
56
+
57
+ def update_progress(task_id, progress, progress_text=None):
58
+ conn = sqlite3.connect('text_tasks.db')
59
+ c = conn.cursor()
60
+ c.execute('UPDATE text_tasks SET progress = ?, progress_text = ? WHERE id = ?',
61
+ (progress, progress_text, task_id))
62
+ conn.commit()
63
+ conn.close()
64
+
65
+ def update_status(task_id, status, result=None, error=None):
66
+ conn = sqlite3.connect('text_tasks.db')
67
+ c = conn.cursor()
68
+ if status == 'completed':
69
+ c.execute('''UPDATE text_tasks
70
+ SET status = ?, result = ?, processed_at = ?, progress = 100, progress_text = 'Completed'
71
+ WHERE id = ?''',
72
+ (status, result, datetime.now().isoformat(), task_id))
73
+ elif status == 'failed':
74
+ c.execute('''UPDATE text_tasks
75
+ SET status = ?, result = ?, processed_at = ?, progress_text = 'Failed'
76
+ WHERE id = ?''',
77
+ (status, f"Error: {error}", datetime.now().isoformat(), task_id))
78
+ else:
79
+ c.execute('UPDATE text_tasks SET status = ? WHERE id = ?', (status, task_id))
80
+ conn.commit()
81
+ conn.close()
82
+
83
+ def worker_loop():
84
+ """Worker loop: loads Qwen model once, then processes queued tasks."""
85
+ print("πŸ€– TTT Worker starting β€” importing ttt package...")
86
+
87
+ POLL_INTERVAL = 3
88
+
89
+ try:
90
+ from ttt.runner import initiate
91
+ # Warm up: load the engine by doing a tiny call (model load happens inside initiate)
92
+ print("πŸ“₯ Loading Qwen model (this may take a few minutes)...")
93
+ initiate({'text': 'Hi', 'model': 'qwen', 'max_new_tokens': 1})
94
+ print("βœ… Qwen model ready")
95
+ except Exception as e:
96
+ print(f"❌ Failed to load model: {e}")
97
+ return
98
+
99
+ from ttt.runner import initiate
100
+
101
+ print("πŸ€– TTT Worker ready. Monitoring for new tasks...")
102
+
103
+ while worker_running:
104
+ cleanup_old_entries()
105
+ try:
106
+ conn = sqlite3.connect('text_tasks.db')
107
+ conn.row_factory = sqlite3.Row
108
+ c = conn.cursor()
109
+ c.execute('''SELECT * FROM text_tasks
110
+ WHERE status = 'not_started'
111
+ ORDER BY created_at ASC
112
+ LIMIT 1''')
113
+ row = c.fetchone()
114
+ conn.close()
115
+
116
+ if row:
117
+ task_id = row['id']
118
+ input_text = row['input_text']
119
+ system_prompt = row['system_prompt'] or "You are a helpful assistant."
120
+
121
+ print(f"\n{'='*60}")
122
+ print(f"πŸ“ Processing task: {task_id}")
123
+ print(f"πŸ“Œ Input: {input_text[:100]}{'...' if len(input_text) > 100 else ''}")
124
+ print(f"{'='*60}")
125
+
126
+ update_status(task_id, 'processing')
127
+
128
+ def make_progress_cb(tid):
129
+ def cb(percent, text):
130
+ update_progress(tid, percent, text)
131
+ return cb
132
+
133
+ try:
134
+ result = initiate(
135
+ {
136
+ 'text': input_text,
137
+ 'system_prompt': system_prompt,
138
+ 'model': 'qwen',
139
+ },
140
+ progress_callback=make_progress_cb(task_id)
141
+ )
142
+
143
+ if result:
144
+ import json
145
+ print(f"βœ… Task completed: {task_id}")
146
+ print(f"πŸ“„ Output preview: {result.get('text', '')[:100]}...")
147
+ update_status(task_id, 'completed', result=json.dumps(result))
148
+ else:
149
+ raise Exception("initiate() returned empty result")
150
+
151
+ except Exception as e:
152
+ print(f"❌ Task failed: {task_id} β€” {e}")
153
+ update_status(task_id, 'failed', error=str(e))
154
+ else:
155
+ time.sleep(POLL_INTERVAL)
156
+
157
+ except Exception as e:
158
+ print(f"⚠️ Worker error: {e}")
159
+ time.sleep(POLL_INTERVAL)
160
+
161
+ @app.route('/')
162
+ def index():
163
+ return send_from_directory('.', 'index.html')
164
+
165
+ @app.route('/api/submit', methods=['POST'])
166
+ def submit_task():
167
+ data = request.get_json()
168
+ if not data or not data.get('text', '').strip():
169
+ return jsonify({'error': 'No input text provided'}), 400
170
+
171
+ task_id = str(uuid.uuid4())
172
+ input_text = data['text'].strip()
173
+ system_prompt = data.get('system_prompt', '').strip() or None
174
+ hide_from_ui = 1 if data.get('hide_from_ui') else 0
175
+
176
+ conn = sqlite3.connect('text_tasks.db')
177
+ c = conn.cursor()
178
+ c.execute('''INSERT INTO text_tasks
179
+ (id, input_text, system_prompt, status, created_at, hide_from_ui)
180
+ VALUES (?, ?, ?, ?, ?, ?)''',
181
+ (task_id, input_text, system_prompt, 'not_started', datetime.now().isoformat(), hide_from_ui))
182
+ conn.commit()
183
+ conn.close()
184
+
185
+ start_worker()
186
+
187
+ return jsonify({
188
+ 'id': task_id,
189
+ 'status': 'not_started',
190
+ 'message': 'Task submitted successfully'
191
+ }), 201
192
+
193
+ def get_average_processing_time(cursor):
194
+ cursor.execute('''SELECT created_at, processed_at FROM text_tasks
195
+ WHERE status = 'completed' AND processed_at IS NOT NULL
196
+ ORDER BY processed_at DESC LIMIT 20''')
197
+ rows = cursor.fetchall()
198
+ if not rows:
199
+ return 120.0 # default: 2 min per task
200
+
201
+ total, count = 0, 0
202
+ for r in rows:
203
+ try:
204
+ duration = (datetime.fromisoformat(r['processed_at']) -
205
+ datetime.fromisoformat(r['created_at'])).total_seconds()
206
+ if duration > 0:
207
+ total += duration
208
+ count += 1
209
+ except:
210
+ continue
211
+ return total / count if count else 120.0
212
+
213
+ @app.route('/api/tasks', methods=['GET'])
214
+ def get_tasks():
215
+ conn = sqlite3.connect('text_tasks.db')
216
+ conn.row_factory = sqlite3.Row
217
+ c = conn.cursor()
218
+
219
+ avg_time = get_average_processing_time(c)
220
+
221
+ c.execute("SELECT id FROM text_tasks WHERE status = 'not_started' ORDER BY created_at ASC")
222
+ queue_ids = [r['id'] for r in c.fetchall()]
223
+
224
+ c.execute("SELECT COUNT(*) as cnt FROM text_tasks WHERE status = 'processing'")
225
+ processing_count = c.fetchone()['cnt']
226
+
227
+ c.execute('SELECT * FROM text_tasks WHERE hide_from_ui = 0 OR hide_from_ui IS NULL ORDER BY created_at DESC')
228
+ rows = c.fetchall()
229
+ conn.close()
230
+
231
+ tasks = []
232
+ for row in rows:
233
+ queue_position = None
234
+ estimated_start_seconds = None
235
+
236
+ if row['status'] == 'not_started' and row['id'] in queue_ids:
237
+ queue_position = queue_ids.index(row['id']) + 1
238
+ files_ahead = queue_position - 1 + processing_count
239
+ estimated_start_seconds = round(files_ahead * avg_time)
240
+
241
+ tasks.append({
242
+ 'id': row['id'],
243
+ 'input_text': row['input_text'][:200] + ('...' if len(row['input_text']) > 200 else ''),
244
+ 'status': row['status'],
245
+ 'result': "HIDDEN_IN_LIST_VIEW",
246
+ 'created_at': row['created_at'],
247
+ 'processed_at': row['processed_at'],
248
+ 'progress': row['progress'] or 0,
249
+ 'progress_text': row['progress_text'],
250
+ 'queue_position': queue_position,
251
+ 'estimated_start_seconds': estimated_start_seconds
252
+ })
253
+
254
+ return jsonify(tasks)
255
+
256
+ @app.route('/api/tasks/<task_id>', methods=['GET'])
257
+ def get_task(task_id):
258
+ conn = sqlite3.connect('text_tasks.db')
259
+ conn.row_factory = sqlite3.Row
260
+ c = conn.cursor()
261
+ c.execute('SELECT * FROM text_tasks WHERE id = ?', (task_id,))
262
+ row = c.fetchone()
263
+
264
+ if row is None:
265
+ conn.close()
266
+ return jsonify({'error': 'Task not found'}), 404
267
+
268
+ queue_position = None
269
+ estimated_start_seconds = None
270
+
271
+ if row['status'] == 'not_started':
272
+ avg_time = get_average_processing_time(c)
273
+ c.execute("SELECT COUNT(*) as pos FROM text_tasks WHERE status = 'not_started' AND created_at < ?",
274
+ (row['created_at'],))
275
+ queue_position = c.fetchone()['pos'] + 1
276
+ c.execute("SELECT COUNT(*) as cnt FROM text_tasks WHERE status = 'processing'")
277
+ processing_count = c.fetchone()['cnt']
278
+ estimated_start_seconds = round((queue_position - 1 + processing_count) * avg_time)
279
+
280
+ conn.close()
281
+
282
+ return jsonify({
283
+ 'id': row['id'],
284
+ 'input_text': row['input_text'],
285
+ 'status': row['status'],
286
+ 'result': row['result'],
287
+ 'created_at': row['created_at'],
288
+ 'processed_at': row['processed_at'],
289
+ 'progress': row['progress'] or 0,
290
+ 'progress_text': row['progress_text'],
291
+ 'queue_position': queue_position,
292
+ 'estimated_start_seconds': estimated_start_seconds
293
+ })
294
+
295
+ @app.route('/health', methods=['GET'])
296
+ def health():
297
+ return jsonify({
298
+ 'status': 'healthy',
299
+ 'service': 'text-to-text-generator',
300
+ 'worker_running': worker_running
301
+ })
302
+
303
+ if __name__ == '__main__':
304
+ init_db()
305
+ print("\n" + "="*60)
306
+ print("πŸš€ Text-to-Text Generator API Server (Qwen/Qwen3.5-4B)")
307
+ print("="*60)
308
+ print("πŸ“Œ Worker + model load on first task submission")
309
+ print("="*60 + "\n")
310
+
311
+ port = int(os.environ.get('PORT', 7860))
312
+ app.run(debug=False, host='0.0.0.0', port=port)
index.html ADDED
@@ -0,0 +1,716 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+
4
+ <head>
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>Text-to-Text Generator</title>
8
+ <style>
9
+ * {
10
+ margin: 0;
11
+ padding: 0;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ :root {
16
+ --bg: #0a0e27;
17
+ --surface: #141b3d;
18
+ --primary: #00ff88;
19
+ --secondary: #ff00ff;
20
+ --accent: #00d4ff;
21
+ --error: #ff1744;
22
+ --text: #ffffff;
23
+ --border: 4px;
24
+ }
25
+
26
+ body {
27
+ font-family: 'Space Grotesk', 'Courier New', monospace;
28
+ background: var(--bg);
29
+ color: var(--text);
30
+ min-height: 100vh;
31
+ overflow-x: hidden;
32
+ position: relative;
33
+ }
34
+
35
+ body::before {
36
+ content: '';
37
+ position: fixed;
38
+ top: 0;
39
+ left: 0;
40
+ width: 100%;
41
+ height: 100%;
42
+ background:
43
+ radial-gradient(circle at 20% 50%, rgba(0, 255, 136, 0.1) 0%, transparent 50%),
44
+ radial-gradient(circle at 80% 80%, rgba(255, 0, 255, 0.1) 0%, transparent 50%),
45
+ radial-gradient(circle at 40% 20%, rgba(0, 212, 255, 0.1) 0%, transparent 50%);
46
+ pointer-events: none;
47
+ z-index: 0;
48
+ }
49
+
50
+ .container {
51
+ max-width: 1400px;
52
+ margin: 0 auto;
53
+ padding: 2rem;
54
+ position: relative;
55
+ z-index: 1;
56
+ }
57
+
58
+ header {
59
+ text-align: center;
60
+ margin-bottom: 3rem;
61
+ animation: slideDown 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55);
62
+ }
63
+
64
+ @keyframes slideDown {
65
+ from { opacity: 0; transform: translateY(-50px); }
66
+ to { opacity: 1; transform: translateY(0); }
67
+ }
68
+
69
+ h1 {
70
+ font-size: clamp(2rem, 5vw, 4rem);
71
+ font-weight: 900;
72
+ background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 50%, var(--secondary) 100%);
73
+ -webkit-background-clip: text;
74
+ -webkit-text-fill-color: transparent;
75
+ background-clip: text;
76
+ text-transform: uppercase;
77
+ letter-spacing: -2px;
78
+ margin-bottom: 1rem;
79
+ position: relative;
80
+ display: inline-block;
81
+ }
82
+
83
+ h1::after {
84
+ content: '';
85
+ position: absolute;
86
+ bottom: -10px;
87
+ left: 50%;
88
+ transform: translateX(-50%);
89
+ width: 60%;
90
+ height: 6px;
91
+ background: linear-gradient(90deg, transparent, var(--primary), transparent);
92
+ animation: glow 2s ease-in-out infinite;
93
+ }
94
+
95
+ @keyframes glow {
96
+ 0%, 100% { opacity: 0.5; }
97
+ 50% { opacity: 1; }
98
+ }
99
+
100
+ .subtitle {
101
+ font-size: 1.2rem;
102
+ color: var(--accent);
103
+ letter-spacing: 2px;
104
+ }
105
+
106
+ .input-section {
107
+ background: var(--surface);
108
+ border: var(--border) solid var(--primary);
109
+ box-shadow: 8px 8px 0 var(--primary);
110
+ padding: 2rem;
111
+ margin-bottom: 3rem;
112
+ animation: slideUp 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55) 0.2s both;
113
+ }
114
+
115
+ .input-section:hover {
116
+ transform: translate(-2px, -2px);
117
+ box-shadow: 12px 12px 0 var(--primary);
118
+ transition: all 0.3s ease;
119
+ }
120
+
121
+ @keyframes slideUp {
122
+ from { opacity: 0; transform: translateY(50px); }
123
+ to { opacity: 1; transform: translateY(0); }
124
+ }
125
+
126
+ .field-label {
127
+ color: var(--accent);
128
+ font-weight: 900;
129
+ text-transform: uppercase;
130
+ letter-spacing: 1px;
131
+ margin-bottom: 0.5rem;
132
+ font-size: 0.9rem;
133
+ }
134
+
135
+ textarea {
136
+ width: 100%;
137
+ background: var(--bg);
138
+ border: 3px solid var(--accent);
139
+ color: var(--text);
140
+ padding: 1rem;
141
+ font-family: 'Courier New', monospace;
142
+ font-size: 1rem;
143
+ line-height: 1.6;
144
+ resize: vertical;
145
+ margin-bottom: 1.5rem;
146
+ outline: none;
147
+ transition: border-color 0.3s ease;
148
+ }
149
+
150
+ textarea:focus {
151
+ border-color: var(--primary);
152
+ }
153
+
154
+ #inputText {
155
+ min-height: 150px;
156
+ }
157
+
158
+ #systemPrompt {
159
+ min-height: 80px;
160
+ }
161
+
162
+ .btn {
163
+ background: var(--primary);
164
+ color: var(--bg);
165
+ border: var(--border) solid var(--bg);
166
+ padding: 1rem 2rem;
167
+ font-size: 1.1rem;
168
+ font-weight: 900;
169
+ text-transform: uppercase;
170
+ cursor: pointer;
171
+ transition: all 0.2s ease;
172
+ box-shadow: 4px 4px 0 var(--bg);
173
+ letter-spacing: 1px;
174
+ }
175
+
176
+ .btn:hover:not(:disabled) {
177
+ transform: translate(-2px, -2px);
178
+ box-shadow: 6px 6px 0 var(--bg);
179
+ }
180
+
181
+ .btn:active:not(:disabled) {
182
+ transform: translate(2px, 2px);
183
+ box-shadow: 2px 2px 0 var(--bg);
184
+ }
185
+
186
+ .btn:disabled {
187
+ opacity: 0.6;
188
+ cursor: not-allowed;
189
+ }
190
+
191
+ .btn-secondary {
192
+ background: var(--accent);
193
+ }
194
+
195
+ .btn-small {
196
+ padding: 0.5rem 1rem;
197
+ font-size: 0.85rem;
198
+ box-shadow: 3px 3px 0 var(--bg);
199
+ }
200
+
201
+ .btn-small:hover:not(:disabled) {
202
+ box-shadow: 4px 4px 0 var(--bg);
203
+ }
204
+
205
+ .table-section {
206
+ animation: slideUp 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55) 0.4s both;
207
+ }
208
+
209
+ .table-wrapper {
210
+ overflow-x: auto;
211
+ background: var(--surface);
212
+ border: var(--border) solid var(--secondary);
213
+ box-shadow: 8px 8px 0 var(--secondary);
214
+ }
215
+
216
+ table {
217
+ width: 100%;
218
+ border-collapse: collapse;
219
+ }
220
+
221
+ thead {
222
+ background: linear-gradient(135deg, var(--primary), var(--accent));
223
+ }
224
+
225
+ th {
226
+ padding: 1.5rem 1rem;
227
+ text-align: left;
228
+ font-weight: 900;
229
+ text-transform: uppercase;
230
+ letter-spacing: 1px;
231
+ color: var(--bg);
232
+ border-right: 3px solid var(--bg);
233
+ }
234
+
235
+ th:last-child { border-right: none; }
236
+
237
+ tbody tr {
238
+ border-bottom: 2px solid rgba(0, 212, 255, 0.2);
239
+ transition: all 0.3s ease;
240
+ animation: fadeIn 0.5s ease;
241
+ }
242
+
243
+ @keyframes fadeIn {
244
+ from { opacity: 0; }
245
+ to { opacity: 1; }
246
+ }
247
+
248
+ tbody tr:hover { background: rgba(0, 255, 136, 0.1); }
249
+
250
+ td { padding: 1.5rem 1rem; color: var(--text); }
251
+
252
+ .status {
253
+ display: inline-block;
254
+ padding: 0.5rem 1rem;
255
+ border: 3px solid;
256
+ font-weight: 900;
257
+ text-transform: uppercase;
258
+ font-size: 0.85rem;
259
+ letter-spacing: 1px;
260
+ }
261
+
262
+ .status-not_started {
263
+ background: var(--bg);
264
+ border-color: var(--accent);
265
+ color: var(--accent);
266
+ }
267
+
268
+ .status-processing {
269
+ background: var(--bg);
270
+ border-color: var(--primary);
271
+ color: var(--primary);
272
+ animation: pulse 1.5s ease-in-out infinite;
273
+ }
274
+
275
+ @keyframes pulse {
276
+ 0%, 100% { opacity: 1; }
277
+ 50% { opacity: 0.6; }
278
+ }
279
+
280
+ .status-completed {
281
+ background: var(--primary);
282
+ border-color: var(--primary);
283
+ color: var(--bg);
284
+ }
285
+
286
+ .status-failed {
287
+ background: var(--error);
288
+ border-color: var(--error);
289
+ color: var(--text);
290
+ }
291
+
292
+ .empty-state {
293
+ text-align: center;
294
+ padding: 4rem 2rem;
295
+ color: var(--accent);
296
+ font-size: 1.2rem;
297
+ }
298
+
299
+ .progress-bar-wrap {
300
+ background: rgba(0,212,255,0.1);
301
+ border: 2px solid var(--accent);
302
+ height: 8px;
303
+ margin-top: 6px;
304
+ width: 120px;
305
+ }
306
+
307
+ .progress-bar-fill {
308
+ height: 100%;
309
+ background: var(--primary);
310
+ transition: width 0.4s ease;
311
+ }
312
+
313
+ .refresh-btn {
314
+ position: fixed;
315
+ bottom: 2rem;
316
+ right: 2rem;
317
+ width: 60px;
318
+ height: 60px;
319
+ border-radius: 50%;
320
+ background: var(--secondary);
321
+ border: var(--border) solid var(--bg);
322
+ box-shadow: 4px 4px 0 var(--bg);
323
+ cursor: pointer;
324
+ transition: all 0.3s ease;
325
+ display: flex;
326
+ align-items: center;
327
+ justify-content: center;
328
+ font-size: 1.5rem;
329
+ z-index: 1000;
330
+ }
331
+
332
+ .refresh-btn:hover {
333
+ transform: rotate(180deg) scale(1.1);
334
+ box-shadow: 6px 6px 0 var(--bg);
335
+ }
336
+
337
+ /* Modal */
338
+ .modal {
339
+ display: none;
340
+ position: fixed;
341
+ top: 0; left: 0;
342
+ width: 100%; height: 100%;
343
+ background: rgba(10, 14, 39, 0.95);
344
+ z-index: 2000;
345
+ animation: fadeIn 0.3s ease;
346
+ overflow-y: auto;
347
+ }
348
+
349
+ .modal.active {
350
+ display: flex;
351
+ align-items: center;
352
+ justify-content: center;
353
+ padding: 2rem;
354
+ }
355
+
356
+ .modal-content {
357
+ background: var(--surface);
358
+ border: var(--border) solid var(--primary);
359
+ box-shadow: 12px 12px 0 var(--primary);
360
+ max-width: 900px;
361
+ width: 100%;
362
+ max-height: 85vh;
363
+ position: relative;
364
+ animation: modalSlideIn 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
365
+ display: flex;
366
+ flex-direction: column;
367
+ }
368
+
369
+ @keyframes modalSlideIn {
370
+ from { opacity: 0; transform: translateY(-50px) scale(0.9); }
371
+ to { opacity: 1; transform: translateY(0) scale(1); }
372
+ }
373
+
374
+ .modal-header {
375
+ display: flex;
376
+ justify-content: space-between;
377
+ align-items: center;
378
+ padding: 2rem 2rem 1rem 2rem;
379
+ border-bottom: 3px solid var(--primary);
380
+ background: var(--surface);
381
+ position: sticky;
382
+ top: 0;
383
+ z-index: 10;
384
+ }
385
+
386
+ .modal-title {
387
+ font-size: 1.5rem;
388
+ font-weight: 900;
389
+ color: var(--primary);
390
+ text-transform: uppercase;
391
+ }
392
+
393
+ .modal-close {
394
+ background: var(--error);
395
+ color: var(--text);
396
+ border: 3px solid var(--bg);
397
+ width: 40px;
398
+ height: 40px;
399
+ cursor: pointer;
400
+ font-size: 1.5rem;
401
+ font-weight: 900;
402
+ transition: all 0.2s ease;
403
+ box-shadow: 3px 3px 0 var(--bg);
404
+ }
405
+
406
+ .modal-close:hover {
407
+ transform: translate(-2px, -2px);
408
+ box-shadow: 5px 5px 0 var(--bg);
409
+ }
410
+
411
+ .code-block {
412
+ background: var(--bg);
413
+ border: 3px solid var(--accent);
414
+ padding: 1.5rem;
415
+ overflow-y: auto;
416
+ margin: 1.5rem 2rem 2rem 2rem;
417
+ position: relative;
418
+ flex: 1;
419
+ }
420
+
421
+ .code-block code {
422
+ font-family: 'Courier New', monospace;
423
+ color: var(--primary);
424
+ font-size: 0.95rem;
425
+ line-height: 1.6;
426
+ white-space: pre-wrap;
427
+ word-break: break-word;
428
+ }
429
+
430
+ .copy-btn {
431
+ position: sticky;
432
+ top: 0.5rem;
433
+ float: right;
434
+ background: var(--accent);
435
+ color: var(--bg);
436
+ border: 3px solid var(--bg);
437
+ padding: 0.5rem 1rem;
438
+ font-size: 0.8rem;
439
+ font-weight: 900;
440
+ cursor: pointer;
441
+ transition: all 0.2s ease;
442
+ box-shadow: 3px 3px 0 var(--bg);
443
+ z-index: 5;
444
+ }
445
+
446
+ .copy-btn:hover { transform: translate(-2px, -2px); box-shadow: 4px 4px 0 var(--bg); }
447
+ .copy-btn.copied { background: var(--primary); }
448
+
449
+ .notification {
450
+ position: fixed;
451
+ top: 2rem;
452
+ right: 2rem;
453
+ padding: 1.5rem 2rem;
454
+ background: var(--primary);
455
+ color: var(--bg);
456
+ border: var(--border) solid var(--bg);
457
+ box-shadow: 6px 6px 0 var(--bg);
458
+ font-weight: 900;
459
+ z-index: 2000;
460
+ animation: slideInRight 0.5s ease, slideOutRight 0.5s ease 3.5s;
461
+ }
462
+
463
+ @keyframes slideInRight {
464
+ from { transform: translateX(400px); opacity: 0; }
465
+ to { transform: translateX(0); opacity: 1; }
466
+ }
467
+
468
+ @keyframes slideOutRight {
469
+ to { transform: translateX(400px); opacity: 0; }
470
+ }
471
+
472
+ @media (max-width: 768px) {
473
+ .container { padding: 1rem; }
474
+ .input-section, .table-wrapper { box-shadow: 4px 4px 0 var(--primary); }
475
+ th, td { padding: 1rem 0.5rem; font-size: 0.9rem; }
476
+ .modal-content { padding: 0; }
477
+ .code-block { margin: 1rem; }
478
+ }
479
+ </style>
480
+ </head>
481
+
482
+ <body>
483
+ <div class="container">
484
+ <header>
485
+ <h1>Text-to-Text Generator</h1>
486
+ <p class="subtitle">Qwen3.5-9B &bull; Submit &bull; Queue &bull; Generate</p>
487
+ </header>
488
+
489
+ <div class="input-section">
490
+ <h2 style="margin-bottom: 1.5rem; color: var(--primary);">Submit Text Task</h2>
491
+
492
+ <div class="field-label">System Prompt (optional)</div>
493
+ <textarea id="systemPrompt" placeholder="You are a helpful assistant."></textarea>
494
+
495
+ <div class="field-label">Input Text *</div>
496
+ <textarea id="inputText" placeholder="Enter your prompt or text here..."></textarea>
497
+
498
+ <button class="btn" id="submitBtn" style="width: 100%;">
499
+ πŸš€ Submit &amp; Process
500
+ </button>
501
+ </div>
502
+
503
+ <div class="table-section">
504
+ <h2 style="margin-bottom: 1.5rem; color: var(--secondary);">Processing Queue</h2>
505
+ <div class="table-wrapper">
506
+ <table>
507
+ <thead>
508
+ <tr>
509
+ <th>Input</th>
510
+ <th>Status</th>
511
+ <th>Progress</th>
512
+ <th>Est. Wait</th>
513
+ <th>Result</th>
514
+ <th>Created</th>
515
+ <th>Processed</th>
516
+ </tr>
517
+ </thead>
518
+ <tbody id="tasksTable">
519
+ <tr>
520
+ <td colspan="7" class="empty-state">No tasks submitted yet. Enter some text above!</td>
521
+ </tr>
522
+ </tbody>
523
+ </table>
524
+ </div>
525
+ </div>
526
+ </div>
527
+
528
+ <button class="refresh-btn" id="refreshBtn" title="Refresh">πŸ”„</button>
529
+
530
+ <!-- Modal -->
531
+ <div class="modal" id="resultModal">
532
+ <div class="modal-content">
533
+ <div class="modal-header">
534
+ <div class="modal-title">πŸ“„ Result</div>
535
+ <button class="modal-close" onclick="closeModal()">Γ—</button>
536
+ </div>
537
+ <div class="code-block">
538
+ <button class="copy-btn" onclick="copyResult()">πŸ“‹ Copy</button>
539
+ <code id="resultCode"></code>
540
+ </div>
541
+ </div>
542
+ </div>
543
+
544
+ <script>
545
+ const API_URL = '/api';
546
+ const resultStore = new Map();
547
+
548
+ const submitBtn = document.getElementById('submitBtn');
549
+ const inputText = document.getElementById('inputText');
550
+ const systemPrompt = document.getElementById('systemPrompt');
551
+
552
+ async function submitTask() {
553
+ const text = inputText.value.trim();
554
+ if (!text) {
555
+ showNotification('Please enter some input text.', 'error');
556
+ return;
557
+ }
558
+
559
+ submitBtn.disabled = true;
560
+ submitBtn.textContent = '⏳ Submitting...';
561
+
562
+ try {
563
+ const response = await fetch(`${API_URL}/submit`, {
564
+ method: 'POST',
565
+ headers: { 'Content-Type': 'application/json' },
566
+ body: JSON.stringify({
567
+ text: text,
568
+ system_prompt: systemPrompt.value.trim() || undefined
569
+ })
570
+ });
571
+
572
+ const data = await response.json();
573
+
574
+ if (response.ok) {
575
+ showNotification(`Task submitted! ID: ${data.id.slice(0, 8)}...`);
576
+ inputText.value = '';
577
+ loadTasks();
578
+ } else {
579
+ showNotification(data.error || 'Submission failed', 'error');
580
+ }
581
+ } catch (error) {
582
+ showNotification('Network error: ' + error.message, 'error');
583
+ } finally {
584
+ submitBtn.disabled = false;
585
+ submitBtn.textContent = 'πŸš€ Submit & Process';
586
+ }
587
+ }
588
+
589
+ submitBtn.addEventListener('click', submitTask);
590
+
591
+ inputText.addEventListener('keydown', (e) => {
592
+ if (e.ctrlKey && e.key === 'Enter') submitTask();
593
+ });
594
+
595
+ async function loadTasks() {
596
+ try {
597
+ const response = await fetch(`${API_URL}/tasks`);
598
+ const tasks = await response.json();
599
+
600
+ const tbody = document.getElementById('tasksTable');
601
+ resultStore.clear();
602
+
603
+ if (tasks.length === 0) {
604
+ tbody.innerHTML = '<tr><td colspan="7" class="empty-state">No tasks submitted yet. Enter some text above!</td></tr>';
605
+ return;
606
+ }
607
+
608
+ tbody.innerHTML = tasks.map(task => {
609
+ if (task.result && task.result !== 'HIDDEN_IN_LIST_VIEW') {
610
+ resultStore.set(task.id, task.result);
611
+ }
612
+
613
+ const inputPreview = task.input_text.length > 60
614
+ ? task.input_text.slice(0, 60) + '...'
615
+ : task.input_text;
616
+
617
+ let estWait = 'β€”';
618
+ if (task.status === 'not_started' && task.estimated_start_seconds !== null) {
619
+ const s = task.estimated_start_seconds;
620
+ if (s < 60) estWait = `${s}s`;
621
+ else if (s < 3600) {
622
+ const m = Math.floor(s / 60), sec = s % 60;
623
+ estWait = sec > 0 ? `${m}m ${sec}s` : `${m}m`;
624
+ } else {
625
+ const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60);
626
+ estWait = m > 0 ? `${h}h ${m}m` : `${h}h`;
627
+ }
628
+ if (task.queue_position) estWait = `#${task.queue_position} (${estWait})`;
629
+ } else if (task.status === 'processing') {
630
+ estWait = '⏳ Processing...';
631
+ }
632
+
633
+ const progress = task.progress || 0;
634
+ const progressHtml = task.status === 'processing' || (task.status === 'not_started' && progress > 0)
635
+ ? `<div>${progress}%</div>
636
+ <div class="progress-bar-wrap">
637
+ <div class="progress-bar-fill" style="width:${progress}%"></div>
638
+ </div>
639
+ <div style="font-size:0.75rem;color:var(--accent);margin-top:4px">${task.progress_text || ''}</div>`
640
+ : task.status === 'completed' ? 'βœ… 100%' : 'β€”';
641
+
642
+ const resultHtml = task.status === 'completed'
643
+ ? `<button class="btn btn-small btn-secondary" onclick="showResult('${task.id}')">Show</button>`
644
+ : task.status === 'failed'
645
+ ? `<button class="btn btn-small" style="background:var(--error)" onclick="showResult('${task.id}')">Error</button>`
646
+ : 'β€”';
647
+
648
+ return `
649
+ <tr>
650
+ <td><strong>${inputPreview}</strong></td>
651
+ <td><span class="status status-${task.status}">${task.status.replace('_', ' ')}</span></td>
652
+ <td>${progressHtml}</td>
653
+ <td>${estWait}</td>
654
+ <td>${resultHtml}</td>
655
+ <td>${new Date(task.created_at).toLocaleString()}</td>
656
+ <td>${task.processed_at ? new Date(task.processed_at).toLocaleString() : 'β€”'}</td>
657
+ </tr>`;
658
+ }).join('');
659
+ } catch (error) {
660
+ console.error('Error loading tasks:', error);
661
+ }
662
+ }
663
+
664
+ async function showResult(taskId) {
665
+ // Fetch full result from individual endpoint
666
+ try {
667
+ const response = await fetch(`${API_URL}/tasks/${taskId}`);
668
+ const task = await response.json();
669
+
670
+ const modal = document.getElementById('resultModal');
671
+ const code = document.getElementById('resultCode');
672
+ code.textContent = task.result || '(no result)';
673
+ resultStore.set(taskId, task.result || '');
674
+ modal.classList.add('active');
675
+ } catch (e) {
676
+ showNotification('Failed to load result', 'error');
677
+ }
678
+ }
679
+
680
+ function closeModal() {
681
+ document.getElementById('resultModal').classList.remove('active');
682
+ }
683
+
684
+ function copyResult() {
685
+ const code = document.getElementById('resultCode');
686
+ const btn = event.target;
687
+ navigator.clipboard.writeText(code.textContent).then(() => {
688
+ const orig = btn.textContent;
689
+ btn.textContent = 'βœ“ Copied!';
690
+ btn.classList.add('copied');
691
+ setTimeout(() => { btn.textContent = orig; btn.classList.remove('copied'); }, 2000);
692
+ });
693
+ }
694
+
695
+ document.getElementById('resultModal').addEventListener('click', (e) => {
696
+ if (e.target.id === 'resultModal') closeModal();
697
+ });
698
+
699
+ document.addEventListener('keydown', (e) => {
700
+ if (e.key === 'Escape') closeModal();
701
+ });
702
+
703
+ document.getElementById('refreshBtn').addEventListener('click', loadTasks);
704
+
705
+ // Auto-refresh every 5 seconds when tasks are processing
706
+ setInterval(async () => {
707
+ const rows = document.querySelectorAll('.status-processing, .status-not_started');
708
+ if (rows.length > 0) loadTasks();
709
+ }, 5000);
710
+
711
+ // Initial load
712
+ loadTasks();
713
+ </script>
714
+ </body>
715
+
716
+ </html>
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ Flask==3.0.0
2
+ flask-cors==4.0.0
3
+ werkzeug==3.0.1
4
+
5
+ # TTT - Qwen model
6
+ git+https://github.com/jebin2/TTT.git#egg=ttt-runner[qwen]