Basavaraj
deploy timetable slm api
c120125
Raw
History Blame Contribute Delete
10.3 kB
# main.py β€” Timetable SLM API (HuggingFace Spaces deployment)
import os, re, json, time
from typing import List
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
# ── Globals ───────────────────────────────────────────────────────────────
MODEL = None
TOKENIZER = None
DEVICE = None
# These are set as HuggingFace Space Secrets
HF_REPO = os.environ.get('HF_REPO', 'vishwasmsme/timetable-slm')
HF_TOKEN = os.environ.get('HF_TOKEN', '')
MODEL_DIR = os.environ.get('MODEL_DIR', '/tmp/timetable_slm_model')
MAX_INPUT = 96
MAX_OUTPUT = 200
PROMPT_PREFIX = (
'Convert the following timetable instruction into a structured JSON constraint. '
'Only return valid JSON, no explanation.\n\nInstruction: '
)
CONSTRAINT_SCHEMA = {
'FACULTY_UNAVAILABLE': {'faculty_id':None,'days':None,'slots':None},
'FACULTY_PREFERRED_TIME': {'faculty_id':None,'period':None,'days':None,'priority':'SOFT'},
'FACULTY_MAX_DAILY_HOURS': {'faculty_id':None,'max_hours':None},
'FACULTY_NO_CONSECUTIVE': {'faculty_id':None,'max_consecutive':None},
'FACULTY_FREE_DAY': {'faculty_id':None,'preferred_day':None},
'SUBJECT_PREFERRED_TIME': {'subject_code':None,'period':None,'section_id':None,'priority':'SOFT'},
'SUBJECT_SPACING': {'subject_code':None,'section_id':None,'min_gap_days':None},
'SUBJECT_AVOID_SLOT': {'subject_code':None,'section_id':None,'avoid_first':False,'avoid_last':False},
'LAB_MUST_CONSECUTIVE': {'subject_code':None,'section_id':None,'num_slots':2},
'HEAVY_SUBJECT_MORNING': {'section_id':None},
'NO_BACK_TO_BACK_SUBJECTS': {'subject_codes':None,'section_id':None},
'SECTION_PREFERRED_TIME': {'section_id':None,'period':None,'priority':'SOFT'},
'SECTION_FREE_SLOT': {'section_id':None,'slot':None,'days':None},
'SECTION_MAX_DAILY_SUBJECTS': {'section_id':None,'max_subjects':None},
'LUNCH_BREAK': {'slot':None,'sections':None},
'ROOM_PREFERENCE': {'subject_code':None,'faculty_id':None,'section_id':None,'room_id':None},
'ELECTIVE_SAME_SLOT': {'elective_group':None,'days':None},
'DISTRIBUTE_SUBJECTS_EVENLY': {'subject_code':None,'section_id':None},
'WORKING_DAYS': {'days':None},
}
DAY_LOOKUP = {
'monday':'MON','mon':'MON','tuesday':'TUE','tue':'TUE',
'wednesday':'WED','wed':'WED','thursday':'THU','thu':'THU',
'friday':'FRI','fri':'FRI','saturday':'SAT','sat':'SAT',
}
PERIOD_LOOKUP = {
'morning':'MORNING','early':'MORNING','first half':'MORNING',
'afternoon':'AFTERNOON','post-lunch':'AFTERNOON','second half':'AFTERNOON',
}
# ── Startup ───────────────────────────────────────────────────────────────
@asynccontextmanager
async def lifespan(app: FastAPI):
global MODEL, TOKENIZER, DEVICE
try:
import torch
from transformers import T5ForConditionalGeneration, T5Tokenizer
from huggingface_hub import snapshot_download
print(f'HF_REPO = {HF_REPO}')
print(f'HF_TOKEN = {"SET βœ…" if HF_TOKEN else "NOT SET ❌"}')
print(f'MODEL_DIR = {MODEL_DIR}')
if not os.path.exists(os.path.join(MODEL_DIR, 'config.json')):
print('Downloading model from HuggingFace...')
os.makedirs(MODEL_DIR, exist_ok=True)
snapshot_download(
repo_id=HF_REPO,
local_dir=MODEL_DIR,
token=HF_TOKEN if HF_TOKEN else None,
ignore_patterns=['*.git*'],
)
print('βœ… Model downloaded')
else:
print('βœ… Model already cached')
TOKENIZER = T5Tokenizer.from_pretrained(MODEL_DIR)
MODEL = T5ForConditionalGeneration.from_pretrained(MODEL_DIR)
DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'
MODEL.to(DEVICE).eval()
print(f'βœ… Model ready on {DEVICE}')
except Exception as e:
print(f'❌ STARTUP ERROR: {e}')
import traceback
traceback.print_exc()
raise
yield
# ── App ───────────────────────────────────────────────────────────────────
app = FastAPI(
title='Timetable SLM API',
description='Converts natural language timetable instructions into JSON constraints.',
version='1.0.0',
lifespan=lifespan,
)
app.add_middleware(CORSMiddleware, allow_origins=['*'],
allow_methods=['*'], allow_headers=['*'])
# ── Request models ────────────────────────────────────────────────────────
class SingleRequest(BaseModel):
instruction: str
class BatchRequest(BaseModel):
instructions: List[str]
# ── Helpers ───────────────────────────────────────────────────────────────
def _infer(prompt):
import torch
inp = TOKENIZER(PROMPT_PREFIX + prompt, return_tensors='pt',
max_length=MAX_INPUT, truncation=True).to(DEVICE)
with torch.no_grad():
out = MODEL.generate(
**inp, max_new_tokens=MAX_OUTPUT, num_beams=4,
no_repeat_ngram_size=4, repetition_penalty=1.5,
early_stopping=True,
decoder_start_token_id=TOKENIZER.pad_token_id,
eos_token_id=TOKENIZER.eos_token_id,
pad_token_id=TOKENIZER.pad_token_id,
)
return TOKENIZER.decode(out[0], skip_special_tokens=True)
def _parse(text):
text = text.strip()
text = re.sub(r'^```json\s*|^```\s*|```$', '', text, flags=re.MULTILINE).strip()
try: return json.loads(text)
except: pass
t = text
if not t.startswith('{'): t = '{' + t
if not t.endswith('}'): t = t + '}'
try: return json.loads(t)
except: pass
m = re.search(r'\{.*\}', text, re.DOTALL)
if m:
try: return json.loads(m.group())
except: pass
ctype = re.search(r'"type"\s*:\s*"([^"]+)"', text)
if ctype:
obj = {'type': ctype.group(1)}
for f in ['faculty_id','subject_code','section_id','elective_group','room_id']:
m2 = re.search(f'"{f}"\\s*:\\s*"([^"]+)"', text)
if m2: obj[f] = m2.group(1)
for f in ['max_hours','min_gap_days','num_slots','slot','max_subjects','max_consecutive']:
m2 = re.search(f'"{f}"\\s*:\\s*(\\d+)', text)
if m2: obj[f] = int(m2.group(1))
return {'constraints': [obj]}
return None
def _fix(c, prompt):
c = dict(c)
ctype = c.get('type','').upper()
c['type'] = ctype
if 'DAYS' in c and 'days' not in c: c['days'] = c.pop('DAYS')
if ctype in ('FACULTY_PREFERRED_TIME','SUBJECT_PREFERRED_TIME','SECTION_PREFERRED_TIME'):
if not c.get('priority'): c['priority'] = 'SOFT'
if not c.get('period'):
p = prompt.lower()
for w, v in PERIOD_LOOKUP.items():
if w in p: c['period'] = v; break
if ctype in ('FACULTY_UNAVAILABLE','FACULTY_PREFERRED_TIME') and not c.get('days'):
found = []
for w, code in DAY_LOOKUP.items():
if w in prompt.lower() and code not in found: found.append(code)
if found: c['days'] = found
if ctype == 'FACULTY_FREE_DAY' and not c.get('preferred_day'):
for w, code in DAY_LOOKUP.items():
if w in prompt.lower(): c['preferred_day'] = code; break
if ctype == 'SUBJECT_AVOID_SLOT':
if c.get('avoid_first') is None: c['avoid_first'] = False
if c.get('avoid_last') is None: c['avoid_last'] = False
if ctype == 'LAB_MUST_CONSECUTIVE' and not c.get('num_slots'):
c['num_slots'] = 2
for field, default in CONSTRAINT_SCHEMA.get(ctype, {}).items():
if field not in c: c[field] = default
return c
def _process(instruction):
t0 = time.time()
raw = _infer(instruction)
parsed = _parse(raw)
ms = round((time.time() - t0) * 1000)
if not parsed:
return {'success':False,'instruction':instruction,
'error':'parse_failed','raw':raw,'latency_ms':ms}
fixed = [_fix(c, instruction) for c in parsed.get('constraints', [])]
return {'success':True,'instruction':instruction,
'constraints':fixed,'raw':raw,'latency_ms':ms}
# ── Endpoints ─────────────────────────────────────────────────────────────
@app.get('/')
def root():
return {
'service': 'Timetable SLM API',
'status': 'running',
'model': HF_REPO,
'device': DEVICE,
'docs': 'Visit /docs for interactive API documentation',
}
@app.get('/health')
def health():
return {'status':'ok','model_loaded':MODEL is not None,'device':DEVICE}
@app.post('/constraint')
def single(req: SingleRequest):
if MODEL is None: raise HTTPException(503, 'Model not loaded')
if not req.instruction.strip(): raise HTTPException(400, 'Empty instruction')
return _process(req.instruction)
@app.post('/constraints/batch')
def batch(req: BatchRequest):
if MODEL is None: raise HTTPException(503, 'Model not loaded')
if not req.instructions: raise HTTPException(400, 'Empty list')
results = [_process(i) for i in req.instructions]
all_c = [c for r in results if r['success'] for c in r['constraints']]
failed = [r['instruction'] for r in results if not r['success']]
return {
'success': len(failed) == 0,
'total': len(results),
'parsed': len(results) - len(failed),
'failed': len(failed),
'failed_list': failed,
'all_constraints': all_c,
'results': results,
}