github-actions[bot] commited on
Commit
1a252b6
·
1 Parent(s): 004fac8

Auto-deploy from GitHub: 5e42323d323d52c3ad4f0ba042c88d1abb55405f

Browse files
Dockerfile CHANGED
@@ -1,26 +1,36 @@
1
  FROM python:3.10-slim
2
 
 
3
  ENV PYTHONUNBUFFERED=1
 
4
 
 
5
  WORKDIR /app
6
 
 
7
  RUN apt-get update && apt-get install -y \
8
  git \
9
  curl \
10
  build-essential \
11
  zstd \
 
12
  && rm -rf /var/lib/apt/lists/*
13
 
14
  # Install Ollama
15
  RUN curl -fsSL https://ollama.com/install.sh | sh
16
 
17
- COPY requirements.txt .
18
- RUN pip install --no-cache-dir -r requirements.txt
19
-
20
  COPY . .
21
 
22
- RUN mkdir -p temp_dir
 
 
 
 
23
 
 
24
  EXPOSE 7860
25
 
26
- CMD ["python", "app.py"]
 
 
1
  FROM python:3.10-slim
2
 
3
+ # Set environment variables
4
  ENV PYTHONUNBUFFERED=1
5
+ ENV OLLAMA_HOST=0.0.0.0
6
 
7
+ # Set working directory
8
  WORKDIR /app
9
 
10
+ # Install system dependencies
11
  RUN apt-get update && apt-get install -y \
12
  git \
13
  curl \
14
  build-essential \
15
  zstd \
16
+ procps \
17
  && rm -rf /var/lib/apt/lists/*
18
 
19
  # Install Ollama
20
  RUN curl -fsSL https://ollama.com/install.sh | sh
21
 
22
+ # Copy project files
23
+ COPY pyproject.toml .
 
24
  COPY . .
25
 
26
+ # Install dependencies and project
27
+ RUN pip install --no-cache-dir .
28
+
29
+ # Create necessary directories
30
+ RUN mkdir -p uploads temp_dir
31
 
32
+ # Expose port
33
  EXPOSE 7860
34
 
35
+ # Run the FastAPI app (worker starts automatically on first task)
36
+ CMD ["python", "run.py"]
app.py DELETED
@@ -1,312 +0,0 @@
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)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # TTT App Package
app/api/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # TTT API Package
app/api/routes.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Request, HTTPException
2
+ from fastapi.responses import JSONResponse, FileResponse
3
+ import uuid
4
+ from app.core.config import settings
5
+ from app.db import crud
6
+ from app.services.worker import start_worker, is_worker_running
7
+ from custom_logger import logger_config as logger
8
+
9
+ router = APIRouter()
10
+
11
+ @router.get("/")
12
+ async def index():
13
+ return FileResponse('index.html')
14
+
15
+ @router.post("/api/tasks/upload")
16
+ async def submit_task(request: Request):
17
+ data = await request.json()
18
+ if not data or not data.get('text', '').strip():
19
+ raise HTTPException(status_code=400, detail="No input text provided")
20
+
21
+ task_id = str(uuid.uuid4())
22
+ input_text = data['text'].strip()
23
+ system_prompt = data.get('system_prompt', '').strip() or None
24
+ hide_from_ui = 1 if data.get('hide_from_ui') else 0
25
+
26
+ await crud.insert_task(task_id, input_text, system_prompt, 'not_started', hide_from_ui)
27
+
28
+ await start_worker()
29
+
30
+ return JSONResponse(status_code=201, content={
31
+ 'id': task_id,
32
+ 'filename': input_text[:50] + ("..." if len(input_text) > 50 else ""),
33
+ 'status': 'not_started',
34
+ 'message': 'Task submitted successfully'
35
+ })
36
+
37
+ @router.get("/api/tasks")
38
+ async def get_tasks():
39
+ rows, queue_ids, processing_count, avg_time = await crud.get_all_tasks()
40
+
41
+ tasks = []
42
+ for row in rows:
43
+ queue_position = None
44
+ estimated_start_seconds = None
45
+
46
+ if row['status'] == 'not_started' and row['id'] in queue_ids:
47
+ queue_position = queue_ids.index(row['id']) + 1
48
+ tasks_ahead = queue_position - 1 + processing_count
49
+ estimated_start_seconds = round(tasks_ahead * avg_time)
50
+
51
+ tasks.append({
52
+ 'id': row['id'],
53
+ 'filename': row['input_text'][:200] + ('...' if len(row['input_text']) > 200 else ''),
54
+ 'status': row['status'],
55
+ 'result': "HIDDEN_IN_LIST_VIEW",
56
+ 'created_at': row['created_at'],
57
+ 'processed_at': row['processed_at'],
58
+ 'progress': row['progress'] or 0,
59
+ 'progress_text': row['progress_text'],
60
+ 'queue_position': queue_position,
61
+ 'estimated_start_seconds': estimated_start_seconds
62
+ })
63
+
64
+ return tasks
65
+
66
+ @router.get("/api/tasks/{task_id}")
67
+ async def get_task(task_id: str):
68
+ result = await crud.get_task_by_id(task_id)
69
+ if not result:
70
+ raise HTTPException(status_code=404, detail="Task not found")
71
+
72
+ row, queue_position, estimated_start_seconds = result
73
+
74
+ return {
75
+ 'id': row['id'],
76
+ 'filename': row['input_text'],
77
+ 'status': row['status'],
78
+ 'result': row['result'],
79
+ 'created_at': row['created_at'],
80
+ 'processed_at': row['processed_at'],
81
+ 'progress': row['progress'] or 0,
82
+ 'progress_text': row['progress_text'],
83
+ 'queue_position': queue_position,
84
+ 'estimated_start_seconds': estimated_start_seconds
85
+ }
86
+
87
+ @router.get("/health")
88
+ async def health():
89
+ return {
90
+ 'status': 'healthy',
91
+ 'service': 'ttt-runner',
92
+ 'worker_running': is_worker_running()
93
+ }
app/core/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # TTT Core Package
app/core/config.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ class Config:
4
+ PORT = int(os.environ.get('PORT', 7860))
5
+ UPLOAD_FOLDER = 'uploads'
6
+ TEMP_DIR = 'temp_dir'
7
+ DATABASE_FILE = 'text_tasks.db'
8
+
9
+ # Core logic settings
10
+ POLL_INTERVAL = 3
11
+ CLEANUP_DAYS = 10
12
+
13
+ settings = Config()
14
+
15
+ os.makedirs(settings.UPLOAD_FOLDER, exist_ok=True)
16
+ os.makedirs(settings.TEMP_DIR, exist_ok=True)
app/db/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # TTT DB Package
app/db/crud.py ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import aiosqlite
2
+ from datetime import datetime, timedelta
3
+ from app.core.config import settings
4
+ from custom_logger import logger_config as logger
5
+
6
+ async def insert_task(task_id: str, input_text: str, system_prompt: str, status: str, hide_from_ui: int):
7
+ async with aiosqlite.connect(settings.DATABASE_FILE) as db:
8
+ await db.execute('''INSERT INTO text_tasks
9
+ (id, input_text, system_prompt, status, created_at, hide_from_ui)
10
+ VALUES (?, ?, ?, ?, ?, ?)''',
11
+ (task_id, input_text, system_prompt, status, datetime.now().isoformat(), hide_from_ui))
12
+ await db.commit()
13
+ logger.debug(f"Inserted task (ID: {task_id}) into database.")
14
+
15
+ async def update_status(task_id: str, status: str, result: str = None, error: str = None):
16
+ async with aiosqlite.connect(settings.DATABASE_FILE) as db:
17
+ if status == 'completed':
18
+ await db.execute('''UPDATE text_tasks
19
+ SET status = ?, result = ?, processed_at = ?, progress = 100, progress_text = 'Completed'
20
+ WHERE id = ?''',
21
+ (status, result, datetime.now().isoformat(), task_id))
22
+ logger.info(f"Task ID {task_id} marked as completed.")
23
+ elif status == 'failed':
24
+ await db.execute('''UPDATE text_tasks
25
+ SET status = ?, result = ?, processed_at = ?, progress_text = 'Failed'
26
+ WHERE id = ?''',
27
+ (status, f"Error: {error}", datetime.now().isoformat(), task_id))
28
+ logger.error(f"Task ID {task_id} marked as failed. Error: {error}")
29
+ else:
30
+ await db.execute('UPDATE text_tasks SET status = ? WHERE id = ?', (status, task_id))
31
+ logger.debug(f"Task ID {task_id} status updated to {status}.")
32
+ await db.commit()
33
+
34
+ async def update_progress(task_id: str, progress: int, progress_text: str = None):
35
+ async with aiosqlite.connect(settings.DATABASE_FILE) as db:
36
+ await db.execute('UPDATE text_tasks SET progress = ?, progress_text = ? WHERE id = ?',
37
+ (progress, progress_text, task_id))
38
+ await db.commit()
39
+ logger.debug(f"Task ID {task_id} progress updated to {progress}% ({progress_text}).")
40
+
41
+ async def get_next_not_started():
42
+ async with aiosqlite.connect(settings.DATABASE_FILE) as db:
43
+ db.row_factory = aiosqlite.Row
44
+ async with db.execute('''SELECT * FROM text_tasks
45
+ WHERE status = 'not_started'
46
+ ORDER BY created_at ASC
47
+ LIMIT 1''') as cursor:
48
+ return await cursor.fetchone()
49
+
50
+ async def cleanup_old_entries():
51
+ try:
52
+ async with aiosqlite.connect(settings.DATABASE_FILE) as db:
53
+ db.row_factory = aiosqlite.Row
54
+ cutoff_date = (datetime.now() - timedelta(days=settings.CLEANUP_DAYS)).isoformat()
55
+
56
+ async with db.execute('''DELETE FROM text_tasks WHERE created_at < ?''', (cutoff_date,)) as cursor:
57
+ deleted_rows = cursor.rowcount
58
+ await db.commit()
59
+
60
+ if deleted_rows > 0:
61
+ logger.info(f"Cleanup: Deleted {deleted_rows} old entries (older than {settings.CLEANUP_DAYS} days)")
62
+ except Exception as e:
63
+ logger.error(f"Cleanup error: {e}")
64
+
65
+ async def get_average_processing_time():
66
+ async with aiosqlite.connect(settings.DATABASE_FILE) as db:
67
+ db.row_factory = aiosqlite.Row
68
+ async with db.execute('''SELECT created_at, processed_at FROM text_tasks
69
+ WHERE status = 'completed' AND processed_at IS NOT NULL
70
+ ORDER BY processed_at DESC LIMIT 20''') as cursor:
71
+ completed_rows = await cursor.fetchall()
72
+
73
+ if not completed_rows:
74
+ return 30.0
75
+
76
+ total_seconds = 0
77
+ count = 0
78
+ for r in completed_rows:
79
+ try:
80
+ created = datetime.fromisoformat(r['created_at'])
81
+ processed = datetime.fromisoformat(r['processed_at'])
82
+ duration = (processed - created).total_seconds()
83
+ if duration > 0:
84
+ total_seconds += duration
85
+ count += 1
86
+ except:
87
+ continue
88
+
89
+ return total_seconds / count if count > 0 else 30.0
90
+
91
+ async def get_all_tasks():
92
+ async with aiosqlite.connect(settings.DATABASE_FILE) as db:
93
+ db.row_factory = aiosqlite.Row
94
+
95
+ avg_time = await get_average_processing_time()
96
+
97
+ async with db.execute('''SELECT id FROM text_tasks
98
+ WHERE status = 'not_started'
99
+ ORDER BY created_at ASC''') as cursor:
100
+ queue_ids = [row['id'] for row in await cursor.fetchall()]
101
+
102
+ async with db.execute('''SELECT COUNT(*) as count FROM text_tasks WHERE status = 'processing' ''') as cursor:
103
+ row = await cursor.fetchone()
104
+ processing_count = row['count']
105
+
106
+ async with db.execute('SELECT * FROM text_tasks WHERE hide_from_ui = 0 OR hide_from_ui IS NULL ORDER BY created_at DESC') as cursor:
107
+ rows = await cursor.fetchall()
108
+
109
+ return rows, queue_ids, processing_count, avg_time
110
+
111
+ async def get_task_by_id(task_id: str):
112
+ async with aiosqlite.connect(settings.DATABASE_FILE) as db:
113
+ db.row_factory = aiosqlite.Row
114
+ async with db.execute('SELECT * FROM text_tasks WHERE id = ?', (task_id,)) as cursor:
115
+ row = await cursor.fetchone()
116
+
117
+ if not row:
118
+ return None
119
+
120
+ queue_position = None
121
+ estimated_start_seconds = None
122
+
123
+ if row['status'] == 'not_started':
124
+ avg_time = await get_average_processing_time()
125
+
126
+ async with db.execute('''SELECT COUNT(*) as position FROM text_tasks
127
+ WHERE status = 'not_started' AND created_at < ?''',
128
+ (row['created_at'],)) as cursor:
129
+ position_row = await cursor.fetchone()
130
+ queue_position = position_row['position'] + 1
131
+
132
+ async with db.execute('''SELECT COUNT(*) as count FROM text_tasks WHERE status = 'processing' ''') as cursor:
133
+ count_row = await cursor.fetchone()
134
+ processing_count = count_row['count']
135
+
136
+ tasks_ahead = queue_position - 1 + processing_count
137
+ estimated_start_seconds = round(tasks_ahead * avg_time)
138
+
139
+ return row, queue_position, estimated_start_seconds
app/db/database.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import aiosqlite
2
+ from app.core.config import settings
3
+ from custom_logger import logger_config as logger
4
+
5
+ async def init_db():
6
+ logger.info(f"Initializing database at {settings.DATABASE_FILE}")
7
+ async with aiosqlite.connect(settings.DATABASE_FILE) as db:
8
+ await db.execute('''CREATE TABLE IF NOT EXISTS text_tasks
9
+ (id TEXT PRIMARY KEY,
10
+ input_text TEXT NOT NULL,
11
+ system_prompt TEXT,
12
+ status TEXT NOT NULL,
13
+ result TEXT,
14
+ created_at TEXT NOT NULL,
15
+ processed_at TEXT,
16
+ progress INTEGER DEFAULT 0,
17
+ progress_text TEXT,
18
+ hide_from_ui INTEGER DEFAULT 0)''')
19
+ await db.commit()
20
+ logger.info("Database initialized successfully.")
app/main.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from contextlib import asynccontextmanager
2
+ from fastapi import FastAPI
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+ from app.api.routes import router
5
+ from app.db.database import init_db
6
+ from custom_logger import logger_config as logger
7
+
8
+ @asynccontextmanager
9
+ async def lifespan(app: FastAPI):
10
+ logger.info("="*60)
11
+ logger.info("TTT Runner API Server Starting Up")
12
+ logger.info("="*60)
13
+ logger.info("Worker will start automatically on first task")
14
+ logger.info("="*60)
15
+
16
+ await init_db()
17
+ yield
18
+ logger.info("TTT Runner API Server Shutting Down")
19
+
20
+ app = FastAPI(title="TTT Runner API", version="2.0.0", lifespan=lifespan)
21
+
22
+ app.add_middleware(
23
+ CORSMiddleware,
24
+ allow_origins=["*"],
25
+ allow_credentials=True,
26
+ allow_methods=["*"],
27
+ allow_headers=["*"],
28
+ )
29
+
30
+ app.include_router(router)
app/services/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # TTT Services Package
app/services/worker.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import json
3
+ from app.core.config import settings
4
+ from custom_logger import logger_config as logger
5
+ from app.db import crud
6
+
7
+ worker_task = None
8
+ worker_running = False
9
+
10
+ def is_worker_running():
11
+ return worker_running
12
+
13
+ async def start_worker():
14
+ global worker_task, worker_running
15
+
16
+ logger.info(f"start_worker called: worker_running={worker_running}")
17
+
18
+ if not worker_running:
19
+ worker_running = True
20
+ worker_task = asyncio.create_task(worker_loop())
21
+ logger.info("Worker task started")
22
+ else:
23
+ logger.info("Worker already running")
24
+
25
+ async def worker_loop():
26
+ global worker_running
27
+ logger.info("TTT Worker started. Monitoring for new tasks...")
28
+
29
+ try:
30
+ from ttt.runner import initiate
31
+ # Warm up: load the engine
32
+ loop = asyncio.get_event_loop()
33
+ await loop.run_in_executor(None, lambda: initiate({'text': 'Hi', 'model': 'qwen', 'max_new_tokens': 1}))
34
+ logger.info("✅ Qwen model ready. Monitoring for new tasks...")
35
+ except Exception as e:
36
+ logger.error(f"❌ Failed to load model: {e}")
37
+ worker_running = False
38
+ return
39
+
40
+ while worker_running:
41
+ logger.debug("Worker loop iteration, checking for files...")
42
+ await crud.cleanup_old_entries()
43
+
44
+ try:
45
+ row = await crud.get_next_not_started()
46
+
47
+ if row:
48
+ task_id = row['id']
49
+ input_text = row['input_text']
50
+ system_prompt = row['system_prompt'] or "You are a helpful assistant."
51
+
52
+ logger.info(f"\n{'='*60}\nProcessing task: {task_id}\n📌 Input: {input_text[:100]}...\n{'='*60}")
53
+
54
+ await crud.update_status(task_id, 'processing')
55
+
56
+ loop = asyncio.get_event_loop()
57
+
58
+ def progress_cb(percent, text):
59
+ asyncio.run_coroutine_threadsafe(
60
+ crud.update_progress(task_id, percent, text),
61
+ loop
62
+ )
63
+
64
+ try:
65
+ await crud.update_progress(task_id, 5, "Starting...")
66
+
67
+ result = await loop.run_in_executor(None, lambda: initiate(
68
+ {
69
+ 'text': input_text,
70
+ 'system_prompt': system_prompt,
71
+ 'model': 'qwen',
72
+ },
73
+ progress_callback=progress_cb
74
+ ))
75
+
76
+ if result:
77
+ logger.success(f"Successfully processed: {task_id}")
78
+ await crud.update_status(task_id, 'completed', result=json.dumps(result))
79
+ else:
80
+ raise Exception("initiate() returned empty result")
81
+
82
+ except Exception as e:
83
+ logger.error(f"Failed to process {task_id}: {str(e)}")
84
+ await crud.update_status(task_id, 'failed', error=str(e))
85
+
86
+ else:
87
+ await asyncio.sleep(settings.POLL_INTERVAL)
88
+
89
+ except Exception as e:
90
+ logger.error(f"Worker error: {str(e)}")
91
+ await asyncio.sleep(settings.POLL_INTERVAL)
index.html CHANGED
@@ -4,234 +4,250 @@
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 {
66
- opacity: 0;
67
- transform: translateY(-50px);
68
- }
69
-
70
- to {
71
- opacity: 1;
72
- transform: translateY(0);
73
- }
74
- }
75
-
76
- h1 {
77
- font-size: clamp(2rem, 5vw, 4rem);
78
- font-weight: 900;
79
- background: linear-gradient(135deg, var(--primary) 0%, var(--accent) 50%, var(--secondary) 100%);
80
- -webkit-background-clip: text;
81
- -webkit-text-fill-color: transparent;
82
- background-clip: text;
83
- text-transform: uppercase;
84
- letter-spacing: -2px;
85
- margin-bottom: 1rem;
86
  position: relative;
87
- display: inline-block;
 
88
  }
89
 
90
- h1::after {
91
- content: '';
92
- position: absolute;
93
- bottom: -10px;
94
- left: 50%;
95
- transform: translateX(-50%);
96
- width: 60%;
97
- height: 6px;
98
- background: linear-gradient(90deg, transparent, var(--primary), transparent);
99
- animation: glow 2s ease-in-out infinite;
100
  }
101
 
102
- @keyframes glow {
103
-
104
- 0%,
105
- 100% {
106
- opacity: 0.5;
107
- }
108
-
109
- 50% {
110
- opacity: 1;
111
- }
112
  }
113
 
114
- .subtitle {
115
- font-size: 1.2rem;
116
- color: var(--accent);
117
- letter-spacing: 2px;
 
 
 
118
  }
119
 
120
- .input-section {
121
- background: var(--surface);
122
- border: var(--border) solid var(--primary);
123
- box-shadow: 8px 8px 0 var(--primary);
124
- padding: 2rem;
125
- margin-bottom: 3rem;
126
- animation: slideUp 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55) 0.2s both;
 
 
 
 
127
  }
128
 
129
- .input-section:hover {
130
- transform: translate(-2px, -2px);
131
- box-shadow: 12px 12px 0 var(--primary);
132
- transition: all 0.3s ease;
 
133
  }
134
 
135
- @keyframes slideUp {
136
- from {
137
- opacity: 0;
138
- transform: translateY(50px);
139
- }
 
 
140
 
141
- to {
142
- opacity: 1;
143
- transform: translateY(0);
144
- }
145
  }
146
 
147
  .field-label {
148
- color: var(--accent);
149
- font-weight: 900;
 
150
  text-transform: uppercase;
151
- letter-spacing: 1px;
152
- margin-bottom: 0.5rem;
153
- font-size: 0.9rem;
154
  }
155
 
156
  textarea {
157
  width: 100%;
158
- background: var(--bg);
159
- border: 3px solid var(--accent);
160
- color: var(--text);
161
- padding: 1rem;
162
- font-family: 'Courier New', monospace;
163
- font-size: 1rem;
164
- line-height: 1.6;
165
- resize: vertical;
166
- margin-bottom: 1.5rem;
167
  outline: none;
168
- transition: border-color 0.3s ease;
169
  }
170
 
171
  textarea:focus {
172
- border-color: var(--primary);
173
- }
174
-
175
- #inputText {
176
- min-height: 150px;
177
  }
178
 
179
- #systemPrompt {
180
- min-height: 80px;
181
- }
182
 
183
- .btn {
184
  background: var(--primary);
185
- color: var(--bg);
186
- border: var(--border) solid var(--bg);
187
- padding: 1rem 2rem;
188
- font-size: 1.1rem;
189
- font-weight: 900;
190
- text-transform: uppercase;
191
  cursor: pointer;
192
  transition: all 0.2s ease;
193
- box-shadow: 4px 4px 0 var(--bg);
194
- letter-spacing: 1px;
195
  }
196
 
197
- .btn:hover:not(:disabled) {
198
- transform: translate(-2px, -2px);
199
- box-shadow: 6px 6px 0 var(--bg);
200
  }
201
 
202
- .btn:active:not(:disabled) {
203
- transform: translate(2px, 2px);
204
- box-shadow: 2px 2px 0 var(--bg);
 
 
205
  }
206
 
207
- .btn:disabled {
208
- opacity: 0.6;
209
- cursor: not-allowed;
 
 
210
  }
211
 
212
- .btn-secondary {
213
- background: var(--accent);
 
 
 
214
  }
215
 
216
- .btn-small {
217
- padding: 0.5rem 1rem;
218
- font-size: 0.85rem;
219
- box-shadow: 3px 3px 0 var(--bg);
 
 
220
  }
221
 
222
- .btn-small:hover:not(:disabled) {
223
- box-shadow: 4px 4px 0 var(--bg);
 
 
 
 
 
 
 
 
 
224
  }
225
 
226
- .table-section {
227
- animation: slideUp 0.6s cubic-bezier(0.68, -0.55, 0.265, 1.55) 0.4s both;
 
 
228
  }
229
 
230
- .table-wrapper {
231
- overflow-x: auto;
232
- background: var(--surface);
233
- border: var(--border) solid var(--secondary);
234
- box-shadow: 8px 8px 0 var(--secondary);
 
235
  }
236
 
237
  table {
@@ -239,352 +255,205 @@
239
  border-collapse: collapse;
240
  }
241
 
242
- thead {
243
- background: linear-gradient(135deg, var(--primary), var(--accent));
244
- }
245
-
246
  th {
247
- padding: 1.5rem 1rem;
248
  text-align: left;
249
- font-weight: 900;
 
 
 
250
  text-transform: uppercase;
251
- letter-spacing: 1px;
252
- color: var(--bg);
253
- border-right: 3px solid var(--bg);
254
- }
255
-
256
- th:last-child {
257
- border-right: none;
258
- }
259
-
260
- tbody tr {
261
- border-bottom: 2px solid rgba(0, 212, 255, 0.2);
262
- transition: all 0.3s ease;
263
- animation: fadeIn 0.5s ease;
264
- }
265
-
266
- @keyframes fadeIn {
267
- from {
268
- opacity: 0;
269
- }
270
-
271
- to {
272
- opacity: 1;
273
- }
274
- }
275
-
276
- tbody tr:hover {
277
- background: rgba(0, 255, 136, 0.1);
278
  }
279
 
280
  td {
281
- padding: 1.5rem 1rem;
282
- color: var(--text);
283
- }
284
-
285
- .status {
286
- display: inline-block;
287
- padding: 0.5rem 1rem;
288
- border: 3px solid;
289
- font-weight: 900;
 
 
 
 
290
  text-transform: uppercase;
291
- font-size: 0.85rem;
292
- letter-spacing: 1px;
293
- }
294
-
295
- .status-not_started {
296
- background: var(--bg);
297
- border-color: var(--accent);
298
- color: var(--accent);
299
- }
300
-
301
- .status-processing {
302
- background: var(--bg);
303
- border-color: var(--primary);
304
- color: var(--primary);
305
- animation: pulse 1.5s ease-in-out infinite;
306
- }
307
-
308
- @keyframes pulse {
309
-
310
- 0%,
311
- 100% {
312
- opacity: 1;
313
- }
314
-
315
- 50% {
316
- opacity: 0.6;
317
- }
318
- }
319
-
320
- .status-completed {
321
- background: var(--primary);
322
- border-color: var(--primary);
323
- color: var(--bg);
324
- }
325
-
326
- .status-failed {
327
- background: var(--error);
328
- border-color: var(--error);
329
- color: var(--text);
330
  }
331
 
332
- .empty-state {
333
- text-align: center;
334
- padding: 4rem 2rem;
335
- color: var(--accent);
336
- font-size: 1.2rem;
337
- }
338
-
339
- .progress-bar-wrap {
340
- background: rgba(0, 212, 255, 0.1);
341
- border: 2px solid var(--accent);
342
- height: 8px;
343
- margin-top: 6px;
344
- width: 120px;
345
- }
346
 
347
- .progress-bar-fill {
348
- height: 100%;
349
  background: var(--primary);
350
- transition: width 0.4s ease;
351
- }
352
-
353
- .refresh-btn {
354
- position: fixed;
355
- bottom: 2rem;
356
- right: 2rem;
357
- width: 60px;
358
- height: 60px;
359
- border-radius: 50%;
360
- background: var(--secondary);
361
- border: var(--border) solid var(--bg);
362
- box-shadow: 4px 4px 0 var(--bg);
363
  cursor: pointer;
364
- transition: all 0.3s ease;
365
- display: flex;
366
- align-items: center;
367
- justify-content: center;
368
- font-size: 1.5rem;
369
- z-index: 1000;
370
  }
371
 
372
- .refresh-btn:hover {
373
- transform: rotate(180deg) scale(1.1);
374
- box-shadow: 6px 6px 0 var(--bg);
 
375
  }
376
 
377
  /* Modal */
378
  .modal {
379
- display: none;
380
  position: fixed;
381
- top: 0;
382
- left: 0;
383
- width: 100%;
384
- height: 100%;
385
- background: rgba(10, 14, 39, 0.95);
386
- z-index: 2000;
387
- animation: fadeIn 0.3s ease;
388
- overflow-y: auto;
389
  }
390
 
391
  .modal.active {
392
  display: flex;
393
- align-items: center;
394
- justify-content: center;
395
- padding: 2rem;
396
  }
397
 
398
  .modal-content {
399
  background: var(--surface);
400
- border: var(--border) solid var(--primary);
401
- box-shadow: 12px 12px 0 var(--primary);
402
- max-width: 900px;
403
  width: 100%;
404
- max-height: 85vh;
405
- position: relative;
406
- animation: modalSlideIn 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55);
407
  display: flex;
408
  flex-direction: column;
 
 
409
  }
410
 
411
- @keyframes modalSlideIn {
412
- from {
413
- opacity: 0;
414
- transform: translateY(-50px) scale(0.9);
415
- }
416
-
417
- to {
418
- opacity: 1;
419
- transform: translateY(0) scale(1);
420
- }
421
- }
422
-
 
 
 
 
423
  .modal-header {
 
 
424
  display: flex;
425
  justify-content: space-between;
426
  align-items: center;
427
- padding: 2rem 2rem 1rem 2rem;
428
- border-bottom: 3px solid var(--primary);
429
- background: var(--surface);
430
- position: sticky;
431
- top: 0;
432
- z-index: 10;
433
  }
434
 
435
- .modal-title {
436
- font-size: 1.5rem;
437
- font-weight: 900;
438
- color: var(--primary);
439
- text-transform: uppercase;
 
 
 
 
 
 
 
440
  }
441
 
442
- .modal-close {
443
- background: var(--error);
444
- color: var(--text);
445
- border: 3px solid var(--bg);
446
- width: 40px;
447
- height: 40px;
448
- cursor: pointer;
449
  font-size: 1.5rem;
450
- font-weight: 900;
451
- transition: all 0.2s ease;
452
- box-shadow: 3px 3px 0 var(--bg);
453
- }
454
-
455
- .modal-close:hover {
456
- transform: translate(-2px, -2px);
457
- box-shadow: 5px 5px 0 var(--bg);
458
- }
459
-
460
- .code-block {
461
- background: var(--bg);
462
- border: 3px solid var(--accent);
463
- padding: 1.5rem;
464
- overflow-y: auto;
465
- margin: 1.5rem 2rem 2rem 2rem;
466
- position: relative;
467
- flex: 1;
468
  }
469
 
470
- .code-block code {
471
- font-family: 'Courier New', monospace;
472
- color: var(--primary);
473
- font-size: 0.95rem;
474
- line-height: 1.6;
475
- white-space: pre-wrap;
476
- word-break: break-word;
477
- }
478
 
479
  .copy-btn {
480
- position: sticky;
481
- top: 0.5rem;
482
- float: right;
483
- background: var(--accent);
484
- color: var(--bg);
485
- border: 3px solid var(--bg);
486
- padding: 0.5rem 1rem;
487
- font-size: 0.8rem;
488
- font-weight: 900;
489
  cursor: pointer;
490
- transition: all 0.2s ease;
491
- box-shadow: 3px 3px 0 var(--bg);
492
- z-index: 5;
493
  }
494
 
495
  .copy-btn:hover {
496
- transform: translate(-2px, -2px);
497
- box-shadow: 4px 4px 0 var(--bg);
498
- }
499
-
500
- .copy-btn.copied {
501
- background: var(--primary);
502
- }
503
-
504
- .notification {
505
- position: fixed;
506
- top: 2rem;
507
- right: 2rem;
508
- padding: 1.5rem 2rem;
509
- background: var(--primary);
510
- color: var(--bg);
511
- border: var(--border) solid var(--bg);
512
- box-shadow: 6px 6px 0 var(--bg);
513
- font-weight: 900;
514
- z-index: 2000;
515
- animation: slideInRight 0.5s ease, slideOutRight 0.5s ease 3.5s;
516
- }
517
-
518
- @keyframes slideInRight {
519
- from {
520
- transform: translateX(400px);
521
- opacity: 0;
522
- }
523
-
524
- to {
525
- transform: translateX(0);
526
- opacity: 1;
527
- }
528
  }
529
 
530
- @keyframes slideOutRight {
531
- to {
532
- transform: translateX(400px);
533
- opacity: 0;
534
- }
535
- }
536
-
537
- @media (max-width: 768px) {
538
- .container {
539
- padding: 1rem;
540
- }
541
-
542
- .input-section,
543
- .table-wrapper {
544
- box-shadow: 4px 4px 0 var(--primary);
545
- }
546
-
547
- th,
548
- td {
549
- padding: 1rem 0.5rem;
550
- font-size: 0.9rem;
551
- }
552
-
553
- .modal-content {
554
- padding: 0;
555
- }
556
-
557
- .code-block {
558
- margin: 1rem;
559
- }
560
  }
561
  </style>
562
  </head>
563
 
564
  <body>
565
  <div class="container">
566
- <header>
567
- <h1>Text-to-Text Generator</h1>
568
- <p class="subtitle">Qwen3.5-4B &bull; Submit &bull; Queue &bull; Generate</p>
 
 
 
 
 
 
 
 
 
 
 
569
  </header>
570
 
571
- <div class="input-section">
572
- <h2 style="margin-bottom: 1.5rem; color: var(--primary);">Submit Text Task</h2>
573
-
574
- <div class="field-label">System Prompt (optional)</div>
575
- <textarea id="systemPrompt" placeholder="You are a helpful assistant."></textarea>
 
 
 
 
 
576
 
577
- <div class="field-label">Input Text *</div>
578
- <textarea id="inputText" placeholder="Enter your prompt or text here..."></textarea>
 
 
579
 
580
- <button class="btn" id="submitBtn" style="width: 100%;">
581
- 🚀 Submit &amp; Process
582
- </button>
583
  </div>
584
 
585
- <div class="table-section">
586
- <h2 style="margin-bottom: 1.5rem; color: var(--secondary);">Processing Queue</h2>
587
- <div class="table-wrapper">
 
 
 
 
588
  <table>
589
  <thead>
590
  <tr>
@@ -592,57 +461,63 @@
592
  <th>Status</th>
593
  <th>Progress</th>
594
  <th>Est. Wait</th>
595
- <th>Result</th>
596
- <th>Created</th>
597
- <th>Processed</th>
598
  </tr>
599
  </thead>
600
- <tbody id="tasksTable">
601
- <tr>
602
- <td colspan="7" class="empty-state">No tasks submitted yet. Enter some text above!</td>
603
- </tr>
604
  </tbody>
605
  </table>
606
  </div>
607
  </div>
608
  </div>
609
 
610
- <button class="refresh-btn" id="refreshBtn" title="Refresh">🔄</button>
611
-
612
  <!-- Modal -->
613
  <div class="modal" id="resultModal">
614
  <div class="modal-content">
615
  <div class="modal-header">
616
- <div class="modal-title">📄 Result</div>
617
- <button class="modal-close" onclick="closeModal()">×</button>
 
 
 
618
  </div>
619
- <div class="code-block">
620
- <button class="copy-btn" onclick="copyResult()">📋 Copy</button>
621
- <code id="resultCode"></code>
622
  </div>
623
  </div>
624
  </div>
625
 
626
- <script>
627
- const API_URL = '/api';
628
- const resultStore = new Map();
 
 
 
 
 
629
 
 
 
 
 
630
  const submitBtn = document.getElementById('submitBtn');
631
  const inputText = document.getElementById('inputText');
632
  const systemPrompt = document.getElementById('systemPrompt');
 
 
 
633
 
634
- async function submitTask() {
635
  const text = inputText.value.trim();
636
- if (!text) {
637
- showNotification('Please enter some input text.', 'error');
638
- return;
639
- }
640
 
 
 
641
  submitBtn.disabled = true;
642
- submitBtn.textContent = '⏳ Submitting...';
643
 
644
  try {
645
- const response = await fetch(`${API_URL}/submit`, {
646
  method: 'POST',
647
  headers: { 'Content-Type': 'application/json' },
648
  body: JSON.stringify({
@@ -651,112 +526,82 @@
651
  })
652
  });
653
 
654
- const data = await response.json();
655
-
656
- if (response.ok) {
657
- showNotification(`Task submitted! ID: ${data.id.slice(0, 8)}...`);
658
  inputText.value = '';
659
- loadTasks();
 
 
 
660
  } else {
661
- showNotification(data.error || 'Submission failed', 'error');
 
662
  }
663
- } catch (error) {
664
- showNotification('Network error: ' + error.message, 'error');
 
665
  } finally {
666
  submitBtn.disabled = false;
667
- submitBtn.textContent = '🚀 Submit & Process';
668
  }
669
  }
670
 
671
- submitBtn.addEventListener('click', submitTask);
672
-
673
- inputText.addEventListener('keydown', (e) => {
674
- if (e.ctrlKey && e.key === 'Enter') submitTask();
675
- });
676
 
677
  async function loadTasks() {
678
  try {
679
- const response = await fetch(`${API_URL}/tasks`);
680
- const tasks = await response.json();
681
-
682
- const tbody = document.getElementById('tasksTable');
683
- resultStore.clear();
684
 
685
- if (tasks.length === 0) {
686
- tbody.innerHTML = '<tr><td colspan="7" class="empty-state">No tasks submitted yet. Enter some text above!</td></tr>';
687
- return;
 
 
 
 
 
 
 
 
 
 
688
  }
689
 
690
- tbody.innerHTML = tasks.map(task => {
691
- if (task.result && task.result !== 'HIDDEN_IN_LIST_VIEW') {
692
- resultStore.set(task.id, task.result);
693
- }
694
-
695
- const inputPreview = task.input_text.length > 60
696
- ? task.input_text.slice(0, 60) + '...'
697
- : task.input_text;
698
-
699
- let estWait = '—';
700
- if (task.status === 'not_started' && task.estimated_start_seconds !== null) {
701
- const s = task.estimated_start_seconds;
702
- if (s < 60) estWait = `${s}s`;
703
- else if (s < 3600) {
704
- const m = Math.floor(s / 60), sec = s % 60;
705
- estWait = sec > 0 ? `${m}m ${sec}s` : `${m}m`;
706
- } else {
707
- const h = Math.floor(s / 3600), m = Math.floor((s % 3600) / 60);
708
- estWait = m > 0 ? `${h}h ${m}m` : `${h}h`;
709
- }
710
- if (task.queue_position) estWait = `#${task.queue_position} (${estWait})`;
711
- } else if (task.status === 'processing') {
712
- estWait = '⏳ Processing...';
713
- }
714
 
715
- const progress = task.progress || 0;
716
- const progressHtml = task.status === 'processing' || (task.status === 'not_started' && progress > 0)
717
- ? `<div>${progress}%</div>
718
- <div class="progress-bar-wrap">
719
- <div class="progress-bar-fill" style="width:${progress}%"></div>
720
- </div>
721
- <div style="font-size:0.75rem;color:var(--accent);margin-top:4px">${task.progress_text || ''}</div>`
722
- : task.status === 'completed' ? '✅ 100%' : '—';
723
-
724
- const resultHtml = task.status === 'completed'
725
- ? `<button class="btn btn-small btn-secondary" onclick="showResult('${task.id}')">Show</button>`
726
- : task.status === 'failed'
727
- ? `<button class="btn btn-small" style="background:var(--error)" onclick="showResult('${task.id}')">Error</button>`
728
- : '—';
729
-
730
- return `
731
- <tr>
732
- <td><strong>${inputPreview}</strong></td>
733
- <td><span class="status status-${task.status}">${task.status.replace('_', ' ')}</span></td>
734
- <td>${progressHtml}</td>
735
- <td>${estWait}</td>
736
- <td>${resultHtml}</td>
737
- <td>${new Date(task.created_at).toLocaleString()}</td>
738
- <td>${task.processed_at ? new Date(task.processed_at).toLocaleString() : '—'}</td>
739
- </tr>`;
740
- }).join('');
741
- } catch (error) {
742
- console.error('Error loading tasks:', error);
743
- }
744
- }
745
-
746
- async function showResult(taskId) {
747
- // Fetch full result from individual endpoint
748
  try {
749
- const response = await fetch(`${API_URL}/tasks/${taskId}`);
750
- const task = await response.json();
751
-
752
- const modal = document.getElementById('resultModal');
753
- const code = document.getElementById('resultCode');
754
- code.textContent = task.result || '(no result)';
755
- resultStore.set(taskId, task.result || '');
756
- modal.classList.add('active');
757
- } catch (e) {
758
- showNotification('Failed to load result', 'error');
759
- }
760
  }
761
 
762
  function closeModal() {
@@ -764,34 +609,63 @@
764
  }
765
 
766
  function copyResult() {
767
- const code = document.getElementById('resultCode');
768
- const btn = event.target;
769
- navigator.clipboard.writeText(code.textContent).then(() => {
770
- const orig = btn.textContent;
771
- btn.textContent = '✓ Copied!';
772
- btn.classList.add('copied');
773
- setTimeout(() => { btn.textContent = orig; btn.classList.remove('copied'); }, 2000);
774
  });
775
  }
776
 
777
- document.getElementById('resultModal').addEventListener('click', (e) => {
778
- if (e.target.id === 'resultModal') closeModal();
779
- });
780
-
781
- document.addEventListener('keydown', (e) => {
782
- if (e.key === 'Escape') closeModal();
783
- });
784
-
785
- document.getElementById('refreshBtn').addEventListener('click', loadTasks);
786
-
787
- // Auto-refresh every 5 seconds when tasks are processing
788
- setInterval(async () => {
789
- const rows = document.querySelectorAll('.status-processing, .status-not_started');
790
- if (rows.length > 0) loadTasks();
791
- }, 5000);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
792
 
793
- // Initial load
794
  loadTasks();
 
 
 
795
  </script>
796
  </body>
797
 
 
4
  <head>
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>TTT</title>
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link
11
+ href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Outfit:wght@600;800&family=Permanent+Marker&family=Patrick+Hand&display=swap"
12
+ rel="stylesheet">
13
  <style>
14
+ :root {
15
+ /* Colors */
16
+ --bg: #334155;
17
+ --surface: #5e3329;
18
+ --surface-hover: #7c4436;
19
+ --border: #2d1a15;
20
+ --primary: #2d1a15;
21
+ --primary-light: #a36555;
22
+ --success: #14532d;
23
+ --success-light: #86efac;
24
+ --error: #7f1d1d;
25
+ --error-light: #fca5a5;
26
+ --text-main: #fde6d2;
27
+ --text-bg: #cbd5e1;
28
+ --text-dim: #f5d0b1;
29
+
30
+ /* Fonts */
31
+ --font-main: 'Inter', sans-serif;
32
+ --font-header: 'Patrick Hand', cursive;
33
+ --font-logo: 'Permanent Marker', cursive;
34
+
35
+ /* Sizes & Spacing */
36
+ --border-width: 3px;
37
+ --radius-card: 12px;
38
+ --radius-btn: 8px;
39
+ --radius-pill: 4px;
40
+ --radius-full: 999px;
41
+ --spacing-sm: 0.5rem;
42
+ --spacing-md: 1rem;
43
+ --spacing-lg: 1.5rem;
44
+
45
+ /* Shadows */
46
+ --shadow-card: 4px 4px 0px 0px var(--border);
47
+ --shadow-btn: 3px 3px 0px 0px var(--border);
48
+ }
49
+
50
  * {
51
  margin: 0;
52
  padding: 0;
53
  box-sizing: border-box;
54
+ font-family: var(--font-main);
 
 
 
 
 
 
 
 
 
 
55
  }
56
 
57
  body {
58
+ background-color: var(--bg);
59
+ color: var(--text-bg);
60
+ height: 100vh;
61
+ width: 100vw;
62
+ margin: 0;
63
+ padding: 0;
64
+ display: flex;
65
+ align-items: center;
66
+ justify-content: center;
67
+ overflow: hidden;
68
+ line-height: 1.5;
 
 
 
 
 
 
 
 
 
 
69
  }
70
 
71
  .container {
72
+ width: 90vw;
73
+ height: 90vh;
74
+ display: grid;
75
+ grid-template-columns: repeat(12, 1fr);
76
+ grid-template-rows: auto 1fr;
77
+ gap: 1.5rem;
 
 
 
 
 
78
  }
79
 
80
+ /* Paper Cards */
81
+ .card {
82
+ background: var(--surface);
83
+ border: var(--border-width) solid var(--border);
84
+ border-radius: 255px 15px 225px 15px/15px 225px 15px 255px;
85
+ padding: var(--spacing-lg);
86
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
87
  position: relative;
88
+ overflow: hidden;
89
+ box-shadow: var(--shadow-card);
90
  }
91
 
92
+ .card:hover {
93
+ transform: translate(-2px, -2px);
94
+ box-shadow: 6px 6px 0px 0px var(--border);
 
 
 
 
 
 
 
95
  }
96
 
97
+ /* Header Card */
98
+ .header-card {
99
+ grid-column: span 12;
100
+ display: flex;
101
+ justify-content: space-between;
102
+ align-items: center;
103
+ padding: 1.25rem 2.5rem;
 
 
 
104
  }
105
 
106
+ .logo-group h1 {
107
+ font-family: 'Permanent Marker', cursive;
108
+ font-size: 2.5rem;
109
+ color: var(--text-main);
110
+ letter-spacing: 0.05em;
111
+ transform: rotate(-2deg);
112
+ text-shadow: 2px 2px 0px var(--primary);
113
  }
114
 
115
+ .status-badge {
116
+ display: flex;
117
+ align-items: center;
118
+ gap: 0.5rem;
119
+ font-size: 0.875rem;
120
+ color: var(--text-main);
121
+ font-weight: 700;
122
+ padding: 0.5rem 1rem;
123
+ background: rgba(15, 23, 42, 0.1);
124
+ border-radius: 999px;
125
+ border: 1px solid var(--border);
126
  }
127
 
128
+ .status-dot {
129
+ width: 8px;
130
+ height: 8px;
131
+ border-radius: 50%;
132
+ background: var(--success);
133
  }
134
 
135
+ /* Input Card */
136
+ .input-card {
137
+ grid-column: span 4;
138
+ display: flex;
139
+ flex-direction: column;
140
+ gap: 1.25rem;
141
+ }
142
 
143
+ .field-group {
144
+ display: flex;
145
+ flex-direction: column;
146
+ gap: 0.5rem;
147
  }
148
 
149
  .field-label {
150
+ font-size: 0.75rem;
151
+ font-weight: 800;
152
+ color: var(--text-dim);
153
  text-transform: uppercase;
154
+ letter-spacing: 0.05em;
 
 
155
  }
156
 
157
  textarea {
158
  width: 100%;
159
+ background: rgba(0, 0, 0, 0.2);
160
+ border: var(--border-width) solid var(--border);
161
+ border-radius: var(--radius-btn);
162
+ color: var(--text-main);
163
+ padding: 0.8rem;
164
+ font-family: 'Inter', sans-serif;
165
+ font-size: 0.9rem;
166
+ resize: none;
 
167
  outline: none;
168
+ transition: all 0.2s ease;
169
  }
170
 
171
  textarea:focus {
172
+ border-color: var(--primary-light);
173
+ background: rgba(0, 0, 0, 0.3);
 
 
 
174
  }
175
 
176
+ #systemPrompt { height: 80px; }
177
+ #inputText { flex: 1; min-height: 150px; }
 
178
 
179
+ .submit-btn {
180
  background: var(--primary);
181
+ color: var(--text-main);
182
+ border: var(--border-width) solid var(--border);
183
+ padding: var(--spacing-md);
184
+ border-radius: 255px 15px 225px 15px/15px 225px 15px 255px;
185
+ font-weight: 700;
 
186
  cursor: pointer;
187
  transition: all 0.2s ease;
188
+ box-shadow: var(--shadow-btn);
189
+ text-transform: uppercase;
190
  }
191
 
192
+ .submit-btn:hover {
193
+ transform: translate(-1px, -1px);
194
+ box-shadow: 4px 4px 0px 0px var(--border);
195
  }
196
 
197
+ .submit-btn:disabled {
198
+ opacity: 0.5;
199
+ cursor: not-allowed;
200
+ transform: none;
201
+ box-shadow: none;
202
  }
203
 
204
+ /* Queue Card */
205
+ .queue-card {
206
+ grid-column: span 8;
207
+ display: flex;
208
+ flex-direction: column;
209
  }
210
 
211
+ .card-header {
212
+ display: flex;
213
+ justify-content: space-between;
214
+ align-items: center;
215
+ margin-bottom: 1.25rem;
216
  }
217
 
218
+ .card-title {
219
+ font-size: 1.6rem;
220
+ font-weight: 700;
221
+ font-family: 'Patrick Hand', cursive;
222
+ color: var(--text-main);
223
+ letter-spacing: 0.02em;
224
  }
225
 
226
+ .refresh-btn {
227
+ background: var(--primary);
228
+ border: var(--border-width) solid var(--border);
229
+ color: var(--text-main);
230
+ padding: 0.4rem 1.2rem;
231
+ border-radius: 255px 15px 225px 15px/15px 225px 15px 255px;
232
+ font-size: 0.8rem;
233
+ font-weight: 700;
234
+ cursor: pointer;
235
+ transition: all 0.2s;
236
+ box-shadow: 2px 2px 0px 0px var(--border);
237
  }
238
 
239
+ .refresh-btn:hover {
240
+ transform: translate(-1px, -1px);
241
+ box-shadow: 4px 4px 0px 0px var(--border);
242
+ opacity: 0.95;
243
  }
244
 
245
+ /* Table */
246
+ .table-container {
247
+ flex: 1;
248
+ overflow-y: auto;
249
+ margin-right: -0.5rem;
250
+ padding-right: 0.5rem;
251
  }
252
 
253
  table {
 
255
  border-collapse: collapse;
256
  }
257
 
 
 
 
 
258
  th {
 
259
  text-align: left;
260
+ padding: 0.75rem 0.5rem;
261
+ font-size: 0.8rem;
262
+ font-weight: 800;
263
+ color: var(--text-main);
264
  text-transform: uppercase;
265
+ border-bottom: var(--border-width) solid var(--border);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
266
  }
267
 
268
  td {
269
+ padding: 1rem 0.5rem;
270
+ font-size: 0.875rem;
271
+ font-weight: 600;
272
+ border-bottom: 1px solid rgba(253, 230, 210, 0.1);
273
+ color: var(--text-main);
274
+ }
275
+
276
+ .status-pill {
277
+ padding: 0.25rem 0.6rem;
278
+ border: 2px solid var(--border);
279
+ border-radius: 4px;
280
+ font-size: 0.7rem;
281
+ font-weight: 800;
282
  text-transform: uppercase;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
  }
284
 
285
+ .pill-not_started { background: rgba(15, 23, 42, 0.15); color: var(--text-main); }
286
+ .pill-processing { background: var(--primary-light); color: var(--primary); }
287
+ .pill-completed { background: var(--success-light); color: var(--success); }
288
+ .pill-failed { background: var(--error-light); color: var(--error); }
 
 
 
 
 
 
 
 
 
 
289
 
290
+ .btn-view {
 
291
  background: var(--primary);
292
+ border: 2px solid var(--border);
293
+ color: var(--text-main);
294
+ padding: 0.3rem 1rem;
295
+ border-radius: 255px 15px 225px 15px/15px 225px 15px 255px;
296
+ font-size: 0.75rem;
297
+ font-weight: 700;
 
 
 
 
 
 
 
298
  cursor: pointer;
299
+ transition: all 0.2s;
300
+ box-shadow: 2px 2px 0px 0px var(--border);
 
 
 
 
301
  }
302
 
303
+ .btn-view:hover {
304
+ transform: translate(-1px, -1px);
305
+ box-shadow: 4px 4px 0px 0px var(--border);
306
+ opacity: 0.95;
307
  }
308
 
309
  /* Modal */
310
  .modal {
 
311
  position: fixed;
312
+ inset: 0;
313
+ background: rgba(15, 23, 42, 0.6);
314
+ display: none;
315
+ align-items: center;
316
+ justify-content: center;
317
+ z-index: 100;
318
+ padding: 2rem;
 
319
  }
320
 
321
  .modal.active {
322
  display: flex;
 
 
 
323
  }
324
 
325
  .modal-content {
326
  background: var(--surface);
327
+ border: var(--border-width) solid var(--border);
 
 
328
  width: 100%;
329
+ max-width: 800px;
330
+ border-radius: 255px 15px 225px 15px/15px 225px 15px 255px;
 
331
  display: flex;
332
  flex-direction: column;
333
+ max-height: 80vh;
334
+ box-shadow: var(--shadow-card);
335
  }
336
 
337
+ .status-modal-content {
338
+ max-width: 400px;
339
+ text-align: center;
340
+ padding: var(--spacing-lg);
341
+ }
342
+ .status-icon { margin: 0 auto var(--spacing-md); font-size: 3rem; }
343
+
344
+ /* Utilities */
345
+ .flex-group { display: flex; gap: var(--spacing-md); align-items: center; }
346
+ .btn-api-doc { cursor: pointer; background: var(--primary-light); border-color: var(--primary); }
347
+ .text-bold { font-weight: 700; color: var(--text-main); }
348
+ .text-dimmed { font-size: 0.8rem; color: var(--text-main); opacity: 0.8; }
349
+ .text-center { text-align: center; padding: 3rem; color: var(--text-bg); }
350
+ .progress-container { width: 100px; height: 4px; background: rgba(253, 230, 210, 0.1); border-radius: 2px; overflow: hidden; }
351
+ .progress-bar { height: 100%; background: var(--text-main); }
352
+
353
  .modal-header {
354
+ padding: 1.5rem 2rem;
355
+ border-bottom: 1px solid var(--border);
356
  display: flex;
357
  justify-content: space-between;
358
  align-items: center;
 
 
 
 
 
 
359
  }
360
 
361
+ .modal-body { padding: 2rem; overflow-y: auto; }
362
+
363
+ pre {
364
+ background: #47271f;
365
+ border: var(--border-width) solid var(--border);
366
+ padding: 1.25rem;
367
+ border-radius: 0.75rem;
368
+ font-family: 'Inter', sans-serif;
369
+ font-size: 0.9rem;
370
+ color: var(--text-main);
371
+ white-space: pre-wrap;
372
+ word-break: break-all;
373
  }
374
 
375
+ .close-modal {
376
+ background: transparent;
377
+ border: none;
378
+ color: var(--text-dim);
 
 
 
379
  font-size: 1.5rem;
380
+ cursor: pointer;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
381
  }
382
 
383
+ /* Scrollbar */
384
+ ::-webkit-scrollbar { width: 6px; }
385
+ ::-webkit-scrollbar-track { background: transparent; }
386
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 10px; }
 
 
 
 
387
 
388
  .copy-btn {
389
+ background: var(--primary);
390
+ color: var(--text-main);
391
+ border: 2px solid var(--border);
392
+ padding: 0.2rem 0.8rem;
393
+ border-radius: 255px 15px 225px 15px/15px 225px 15px 255px;
394
+ font-size: 0.7rem;
395
+ font-weight: 700;
 
 
396
  cursor: pointer;
397
+ box-shadow: 2px 2px 0 var(--border);
398
+ transition: all 0.2s;
 
399
  }
400
 
401
  .copy-btn:hover {
402
+ transform: translate(-1px, -1px);
403
+ box-shadow: 3px 3px 0 var(--border);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
404
  }
405
 
406
+ @media (max-width: 900px) {
407
+ .container { grid-template-columns: 1fr; height: auto; overflow-y: auto; padding: 1rem; }
408
+ .card { grid-column: span 12 !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
409
  }
410
  </style>
411
  </head>
412
 
413
  <body>
414
  <div class="container">
415
+ <!-- Header -->
416
+ <header class="card header-card">
417
+ <div class="logo-group">
418
+ <h1>TTT</h1>
419
+ </div>
420
+ <div class="flex-group">
421
+ <div class="status-badge btn-api-doc" id="apiDocBtn">
422
+ <span class="text-bold">API DOC</span>
423
+ </div>
424
+ <div class="status-badge">
425
+ <div class="status-dot" id="healthDot"></div>
426
+ <span id="healthText">Service Online</span>
427
+ </div>
428
+ </div>
429
  </header>
430
 
431
+ <!-- Input -->
432
+ <div class="card input-card">
433
+ <div class="card-header">
434
+ <span class="card-title">Generate</span>
435
+ </div>
436
+
437
+ <div class="field-group">
438
+ <span class="field-label">System Prompt</span>
439
+ <textarea id="systemPrompt" placeholder="You are a helpful assistant."></textarea>
440
+ </div>
441
 
442
+ <div class="field-group" style="flex:1; display:flex; flex-direction:column;">
443
+ <span class="field-label">User Prompt</span>
444
+ <textarea id="inputText" placeholder="What should I generate?"></textarea>
445
+ </div>
446
 
447
+ <button class="submit-btn" id="submitBtn">🚀 Submit Task</button>
 
 
448
  </div>
449
 
450
+ <!-- Queue -->
451
+ <div class="card queue-card">
452
+ <div class="card-header">
453
+ <span class="card-title">Recent Activity</span>
454
+ <button class="refresh-btn" onclick="loadTasks()">Refresh</button>
455
+ </div>
456
+ <div class="table-container">
457
  <table>
458
  <thead>
459
  <tr>
 
461
  <th>Status</th>
462
  <th>Progress</th>
463
  <th>Est. Wait</th>
464
+ <th>Action</th>
 
 
465
  </tr>
466
  </thead>
467
+ <tbody id="queueBody">
468
+ <!-- Data injected here -->
 
 
469
  </tbody>
470
  </table>
471
  </div>
472
  </div>
473
  </div>
474
 
 
 
475
  <!-- Modal -->
476
  <div class="modal" id="resultModal">
477
  <div class="modal-content">
478
  <div class="modal-header">
479
+ <div style="display:flex; align-items:center; gap:1rem;">
480
+ <span class="card-title">Result</span>
481
+ <button class="copy-btn" id="copyBtn" onclick="copyResult()">📋 Copy</button>
482
+ </div>
483
+ <button class="close-modal" onclick="closeModal()">&times;</button>
484
  </div>
485
+ <div class="modal-body">
486
+ <pre id="resultText"></pre>
 
487
  </div>
488
  </div>
489
  </div>
490
 
491
+ <!-- Status Modal -->
492
+ <div class="modal" id="statusModal">
493
+ <div class="modal-content status-modal-content">
494
+ <div class="status-icon">🚀</div>
495
+ <h2 class="card-title" id="statusMessage">Submitting...</h2>
496
+ <p class="text-dimmed" id="statusSubMessage">Please wait while we queue your task.</p>
497
+ </div>
498
+ </div>
499
 
500
+ <script>
501
+ const API_BASE = '/api';
502
+
503
+ // Elements
504
  const submitBtn = document.getElementById('submitBtn');
505
  const inputText = document.getElementById('inputText');
506
  const systemPrompt = document.getElementById('systemPrompt');
507
+ const queueBody = document.getElementById('queueBody');
508
+ const statusModal = document.getElementById('statusModal');
509
+ const statusMessage = document.getElementById('statusMessage');
510
 
511
+ async function uploadTask() {
512
  const text = inputText.value.trim();
513
+ if (!text) return;
 
 
 
514
 
515
+ statusModal.classList.add('active');
516
+ statusMessage.innerText = "Submitting...";
517
  submitBtn.disabled = true;
 
518
 
519
  try {
520
+ const res = await fetch(`${API_BASE}/tasks/upload`, {
521
  method: 'POST',
522
  headers: { 'Content-Type': 'application/json' },
523
  body: JSON.stringify({
 
526
  })
527
  });
528
 
529
+ if (res.ok) {
530
+ statusMessage.innerText = "Success!";
 
 
531
  inputText.value = '';
532
+ setTimeout(() => {
533
+ statusModal.classList.remove('active');
534
+ loadTasks();
535
+ }, 800);
536
  } else {
537
+ statusMessage.innerText = "Submission Failed";
538
+ setTimeout(() => statusModal.classList.remove('active'), 1500);
539
  }
540
+ } catch (err) {
541
+ statusMessage.innerText = "Connection Error";
542
+ setTimeout(() => statusModal.classList.remove('active'), 1500);
543
  } finally {
544
  submitBtn.disabled = false;
 
545
  }
546
  }
547
 
548
+ submitBtn.onclick = uploadTask;
549
+ inputText.onkeydown = (e) => { if (e.ctrlKey && e.key === 'Enter') uploadTask(); };
 
 
 
550
 
551
  async function loadTasks() {
552
  try {
553
+ const res = await fetch(`${API_BASE}/tasks`);
554
+ const tasks = await res.json();
555
+ renderQueue(tasks);
556
+ } catch (err) { console.error(err); }
557
+ }
558
 
559
+ function renderQueue(files) {
560
+ if (files.length === 0) {
561
+ queueBody.innerHTML = '<tr><td colspan="5" class="text-center">No tasks found</td></tr>';
562
+ return;
563
+ }
564
+ queueBody.innerHTML = files.map(f => {
565
+ let estWait = '—';
566
+ if (f.status === 'not_started' && f.estimated_start_seconds !== null) {
567
+ const s = f.estimated_start_seconds;
568
+ estWait = s < 60 ? `${s}s` : `${Math.floor(s/60)}m`;
569
+ if (f.queue_position) estWait = `#${f.queue_position} (${estWait})`;
570
+ } else if (f.status === 'processing') {
571
+ estWait = '⏳ Processing...';
572
  }
573
 
574
+ const preview = f.filename.length > 50 ? f.filename.slice(0, 50) + '...' : f.filename;
575
+
576
+ return `
577
+ <tr>
578
+ <td class="text-bold">${preview}</td>
579
+ <td><span class="status-pill pill-${f.status}">${f.status.replace('_', ' ')}</span></td>
580
+ <td>
581
+ <div class="progress-container">
582
+ <div class="progress-bar" style="width:${f.progress}%"></div>
583
+ </div>
584
+ </td>
585
+ <td>${estWait}</td>
586
+ <td>
587
+ ${f.status === 'completed' ? `<button class="btn-view" onclick="showResult('${f.id}')">View</button>` : '—'}
588
+ </td>
589
+ </tr>`;
590
+ }).join('');
591
+ }
592
+
593
+ async function showResult(id) {
594
+ const res = await fetch(`${API_BASE}/tasks/${id}`);
595
+ const data = await res.json();
596
+ let text = data.result;
 
597
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
598
  try {
599
+ const parsed = JSON.parse(text);
600
+ text = parsed.text || text;
601
+ } catch (e) { }
602
+
603
+ document.getElementById('resultText').innerText = text || '(no result)';
604
+ document.getElementById('resultModal').classList.add('active');
 
 
 
 
 
605
  }
606
 
607
  function closeModal() {
 
609
  }
610
 
611
  function copyResult() {
612
+ const text = document.getElementById('resultText').innerText;
613
+ const btn = document.getElementById('copyBtn');
614
+ navigator.clipboard.writeText(text).then(() => {
615
+ const orig = btn.innerText;
616
+ btn.innerText = '✓ Copied!';
617
+ setTimeout(() => { btn.innerText = orig; }, 2000);
 
618
  });
619
  }
620
 
621
+ document.getElementById('apiDocBtn').onclick = () => {
622
+ const doc = {
623
+ "base_url": window.location.origin,
624
+ "endpoints": [
625
+ {
626
+ "method": "POST",
627
+ "path": "/api/tasks/upload",
628
+ "desc": "Submit text generation task",
629
+ "body": "application/json { text: string, system_prompt?: string }"
630
+ },
631
+ {
632
+ "method": "GET",
633
+ "path": "/api/tasks",
634
+ "desc": "List all tasks"
635
+ },
636
+ {
637
+ "method": "GET",
638
+ "path": "/api/tasks/{task_id}",
639
+ "desc": "Get task details & result"
640
+ },
641
+ {
642
+ "method": "GET",
643
+ "path": "/health",
644
+ "desc": "Service health check"
645
+ }
646
+ ]
647
+ };
648
+ document.getElementById('resultText').innerText = JSON.stringify(doc, null, 2);
649
+ document.querySelector('#resultModal .card-title').innerText = "API Documentation";
650
+ document.getElementById('resultModal').classList.add('active');
651
+ };
652
+
653
+ // Health Check
654
+ async function checkHealth() {
655
+ try {
656
+ const res = await fetch('/health');
657
+ const data = await res.json();
658
+ const dot = document.getElementById('healthDot');
659
+ dot.style.background = data.status === 'healthy' ? 'var(--success)' : 'var(--error)';
660
+ dot.style.boxShadow = `0 0 10px ${data.status === 'healthy' ? 'var(--success)' : 'var(--error)'}`;
661
+ } catch (e) { }
662
+ }
663
 
664
+ // Init
665
  loadTasks();
666
+ setInterval(loadTasks, 5000);
667
+ setInterval(checkHealth, 10000);
668
+ window.onclick = (e) => { if (e.target.classList.contains('modal')) closeModal(); };
669
  </script>
670
  </body>
671
 
pyproject.toml ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ttt-runner"
7
+ version = "2.0.0"
8
+ description = "FastAPI backend for TTT Runner"
9
+ dependencies = [
10
+ "fastapi",
11
+ "uvicorn",
12
+ "aiosqlite",
13
+ "aiofiles",
14
+ "python-multipart",
15
+ "custom_logger @ git+https://github.com/jebin2/custom_logger.git",
16
+ "ttt-runner @ git+https://github.com/jebin2/TTT.git"
17
+ ]
18
+
19
+ [project.scripts]
20
+ ttt-runner = "app.main:app"
requirements.txt DELETED
@@ -1,6 +0,0 @@
1
- Flask==3.0.0
2
- flask-cors==4.0.0
3
- werkzeug==3.0.1
4
-
5
- # TTT
6
- ttt-runner @ git+https://github.com/jebin2/TTT.git
 
 
 
 
 
 
 
run.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import uvicorn
2
+ from app.core.config import settings
3
+
4
+ if __name__ == "__main__":
5
+ uvicorn.run("app.main:app", host="0.0.0.0", port=settings.PORT, reload=False)