Report-Generator / routes /questions.py
root
Working CHanges to revesion ; add preact support
e8a57cb
Raw
History Blame
31.3 kB
import os
from flask import current_app, request, jsonify, render_template, flash, redirect, url_for
from .common import main_bp, get_db_connection, login_required, current_user
from utils import sync_neetprep_collection
from processing import resize_image_if_needed, call_nim_ocr_api, extract_question_number_from_ocr_result
from strings import ROUTE_SAVE_QUESTIONS, ROUTE_EXTRACT_QUESTION_NUMBER, ROUTE_EXTRACT_ALL_QUESTION_NUMBERS, METHOD_POST
import requests
import json
from nvidia_prompts import BIOLOGY_PROMPT_TEMPLATE, CHEMISTRY_PROMPT_TEMPLATE, PHYSICS_PROMPT_TEMPLATE, MATHEMATICS_PROMPT_TEMPLATE, GENERAL_CLASSIFICATION_PROMPT
from utils import parse_note_payload
NVIDIA_API_KEY = os.getenv("NVIDIA_API_KEY")
NVIDIA_NIM_AVAILABLE = bool(NVIDIA_API_KEY)
def get_nvidia_prompt(subject, input_questions):
if not subject or subject.lower() == 'auto':
return GENERAL_CLASSIFICATION_PROMPT.format(input_questions=input_questions)
if subject.lower() == 'biology': return BIOLOGY_PROMPT_TEMPLATE.format(input_questions=input_questions)
if subject.lower() == 'chemistry': return CHEMISTRY_PROMPT_TEMPLATE.format(input_questions=input_questions)
if subject.lower() == 'physics': return PHYSICS_PROMPT_TEMPLATE.format(input_questions=input_questions)
if subject.lower() == 'mathematics': return MATHEMATICS_PROMPT_TEMPLATE.format(input_questions=input_questions)
# Default to general prompt for unknown subjects
return GENERAL_CLASSIFICATION_PROMPT.format(input_questions=input_questions)
@main_bp.route('/get_topic_suggestions', methods=['POST'])
@login_required
def get_topic_suggestions():
data = request.json
question_text, image_id, subject = data.get('question_text'), data.get('image_id'), data.get('subject')
# Subject is now optional - will use general prompt if not provided
if not question_text and image_id:
try:
conn = get_db_connection()
# First check if we already have question_text cached
q_row = conn.execute('SELECT question_text FROM questions WHERE image_id = ?', (image_id,)).fetchone()
if q_row and q_row['question_text']:
question_text = q_row['question_text']
else:
# Get image directly (works even if questions not saved yet)
img_row = conn.execute('SELECT processed_filename, session_id FROM images WHERE id = ?', (image_id,)).fetchone()
if img_row and img_row['processed_filename']:
image_path = os.path.join(current_app.config['PROCESSED_FOLDER'], img_row['processed_filename'])
if os.path.exists(image_path):
question_text = " ".join(item['text_prediction']['text'] for item in call_nim_ocr_api(resize_image_if_needed(image_path))['data'][0]['text_detections'])
# Cache the OCR result if question record exists
if q_row:
conn.execute('UPDATE questions SET question_text = ? WHERE image_id = ?', (question_text, image_id))
conn.commit()
conn.close()
except Exception as e: return jsonify({'error': f"OCR failed: {str(e)}"}), 500
if not question_text: return jsonify({'error': 'Could not obtain question text.'}), 400
prompt_content = get_nvidia_prompt(subject, f"1. {question_text}")
if not NVIDIA_API_KEY: return jsonify({'error': 'NVIDIA_API_KEY not set'}), 500
try:
res = requests.post('https://integrate.api.nvidia.com/v1/chat/completions', headers={'Authorization': f'Bearer {NVIDIA_API_KEY}', 'Accept': 'application/json', 'Content-Type': 'application/json'}, json={"model": "nvidia/nemotron-3-nano-30b-a3b", "messages": [{"content": prompt_content, "role": "user"}], "temperature": 0.2, "top_p": 1, "max_tokens": 1024, "stream": False}, timeout=30)
res.raise_for_status()
content = res.json()['choices'][0]['message']['content']
if "```json" in content: content = content.split("```json")[1].split("```")[0].strip()
elif "```" in content: content = content.split("```")[1].split("```")[0].strip()
parsed_data = json.loads(content)
suggestions = []
detected_subject = subject # Default to input subject
other_subjects = []
if parsed_data.get('data'):
item = parsed_data['data'][0]
# Extract detected subject from AI response
detected_subject = item.get('subject', subject) or subject
# Extract other possible subjects
other_subjects = item.get('other_possible_subjects', [])
if isinstance(other_subjects, str):
other_subjects = [other_subjects]
primary = item.get('chapter_title')
if primary and primary != 'Unclassified':
suggestions.append(primary)
if 'other_possible_chapters' in item:
others = item['other_possible_chapters']
if isinstance(others, list): suggestions.extend([c for c in others if c and c != 'Unclassified'])
# Always return at least one suggestion with fallback
if not suggestions:
fallback_topics = {
'biology': ['Cell: The Unit of Life', 'Biomolecules', 'Human Reproduction', 'Molecular Basis of Inheritance', 'Ecology'],
'chemistry': ['Organic Chemistry – Some Basic Principles and Techniques (GOC)', 'Chemical Bonding and Molecular Structure', 'Thermodynamics', 'Electrochemistry'],
'physics': ['Laws of Motion', 'Work, Energy and Power', 'Current Electricity', 'Electromagnetic Induction'],
'mathematics': ['Calculus', 'Algebra', 'Coordinate Geometry', 'Probability']
}
subj_key = (detected_subject or 'biology').lower()
suggestions = fallback_topics.get(subj_key, ['Unclassified'])
return jsonify({
'success': True,
'suggestions': suggestions[:5],
'subject': detected_subject,
'other_possible_subjects': other_subjects,
'full_response': parsed_data
})
except Exception as e:
return jsonify({
'success': True,
'suggestions': ['Unclassified'],
'subject': subject or 'Biology',
'other_possible_subjects': [],
'error': str(e)
})
@main_bp.route('/get_topic_suggestions_batch', methods=['POST'])
@login_required
def get_topic_suggestions_batch():
"""Batch endpoint for getting topic suggestions for multiple images at once.
Requires a subject to be specified. Processes up to 8 questions in a single API call."""
data = request.json
image_ids = data.get('image_ids', [])
subject = data.get('subject')
current_app.logger.info(f"[BATCH] Received request for {len(image_ids)} images, subject={subject}")
if not image_ids:
return jsonify({'error': 'No image_ids provided'}), 400
if not subject or subject.lower() == 'auto':
return jsonify({'error': 'Subject must be specified for batch requests'}), 400
if len(image_ids) > 8:
return jsonify({'error': 'Maximum 8 images per batch'}), 400
if not NVIDIA_API_KEY:
return jsonify({'error': 'NVIDIA_API_KEY not set'}), 500
try:
conn = get_db_connection()
questions_data = [] # List of {image_id, index, question_text}
for idx, image_id in enumerate(image_ids):
# Verify ownership
owner = conn.execute("SELECT s.user_id FROM images i JOIN sessions s ON i.session_id = s.id WHERE i.id = ?", (image_id,)).fetchone()
if not owner or owner['user_id'] != current_user.id:
current_app.logger.warning(f"[BATCH] Skipping image {image_id}: unauthorized")
continue # Skip unauthorized images
question_text = None
# First check if we already have question_text cached
q_row = conn.execute('SELECT question_text FROM questions WHERE image_id = ?', (image_id,)).fetchone()
if q_row and q_row['question_text']:
question_text = q_row['question_text']
current_app.logger.debug(f"[BATCH] Image {image_id}: using cached question_text")
else:
# Get image directly
img_row = conn.execute('SELECT processed_filename FROM images WHERE id = ?', (image_id,)).fetchone()
if img_row and img_row['processed_filename']:
image_path = os.path.join(current_app.config['PROCESSED_FOLDER'], img_row['processed_filename'])
if os.path.exists(image_path):
try:
current_app.logger.info(f"[BATCH] Image {image_id}: running OCR on {img_row['processed_filename']}")
question_text = " ".join(item['text_prediction']['text'] for item in call_nim_ocr_api(resize_image_if_needed(image_path))['data'][0]['text_detections'])
# Cache the OCR result if question record exists
if q_row:
conn.execute('UPDATE questions SET question_text = ? WHERE image_id = ?', (question_text, image_id))
conn.commit()
except Exception as ocr_err:
current_app.logger.error(f"[BATCH] Image {image_id}: OCR failed - {ocr_err}")
else:
current_app.logger.warning(f"[BATCH] Image {image_id}: file not found at {image_path}")
else:
current_app.logger.warning(f"[BATCH] Image {image_id}: no processed_filename in DB")
if question_text:
questions_data.append({'image_id': image_id, 'index': idx + 1, 'question_text': question_text})
else:
current_app.logger.warning(f"[BATCH] Image {image_id}: no question_text obtained")
conn.close()
current_app.logger.info(f"[BATCH] Got question text for {len(questions_data)}/{len(image_ids)} images")
if not questions_data:
return jsonify({'error': 'Could not obtain question text for any images'}), 400
# Build multi-question prompt
input_questions = "\n".join(f"{q['index']}. {q['question_text']}" for q in questions_data)
prompt_content = get_nvidia_prompt(subject, input_questions)
current_app.logger.info(f"[BATCH] Sending {len(questions_data)} questions to NVIDIA API")
current_app.logger.debug(f"[BATCH] Prompt preview: {input_questions[:500]}...")
# Make single API call for all questions
res = requests.post(
'https://integrate.api.nvidia.com/v1/chat/completions',
headers={'Authorization': f'Bearer {NVIDIA_API_KEY}', 'Accept': 'application/json', 'Content-Type': 'application/json'},
json={"model": "nvidia/nemotron-3-nano-30b-a3b", "messages": [{"content": prompt_content, "role": "user"}], "temperature": 0.2, "top_p": 1, "max_tokens": 2048, "stream": False},
timeout=60
)
res.raise_for_status()
content = res.json()['choices'][0]['message']['content']
current_app.logger.info(f"[BATCH] NVIDIA API response length: {len(content)} chars")
current_app.logger.debug(f"[BATCH] Raw response: {content[:1000]}...")
# Parse JSON from response
if "```json" in content:
content = content.split("```json")[1].split("```")[0].strip()
elif "```" in content:
content = content.split("```")[1].split("```")[0].strip()
parsed_data = json.loads(content)
current_app.logger.info(f"[BATCH] Parsed data has {len(parsed_data.get('data', []))} items")
# Build results for each image
results = {}
fallback_topics = {
'biology': ['Cell: The Unit of Life', 'Biomolecules', 'Human Reproduction'],
'chemistry': ['Organic Chemistry – Some Basic Principles and Techniques (GOC)', 'Chemical Bonding and Molecular Structure'],
'physics': ['Laws of Motion', 'Work, Energy and Power', 'Current Electricity'],
'mathematics': ['Calculus', 'Algebra', 'Coordinate Geometry']
}
if parsed_data.get('data'):
data_items = parsed_data['data']
current_app.logger.info(f"[BATCH] AI returned {len(data_items)} items, we have {len(questions_data)} questions")
# Try to match by index first, then fall back to order-based matching
matched_by_index = 0
for item in data_items:
item_index = item.get('index', 0)
current_app.logger.debug(f"[BATCH] Processing item with index={item_index}: {item}")
# Find the matching question by our sequential index
matching_q = next((q for q in questions_data if q['index'] == item_index), None)
if matching_q:
matched_by_index += 1
suggestions = []
primary = item.get('chapter_title')
if primary and primary != 'Unclassified':
suggestions.append(primary)
if 'other_possible_chapters' in item:
others = item['other_possible_chapters']
if isinstance(others, list):
suggestions.extend([c for c in others if c and c != 'Unclassified'])
if not suggestions:
suggestions = fallback_topics.get(subject.lower(), ['Unclassified'])
results[matching_q['image_id']] = {
'success': True,
'suggestions': suggestions[:5],
'subject': subject,
'other_possible_subjects': []
}
current_app.logger.info(f"[BATCH] Image {matching_q['image_id']}: matched by index, suggestions={suggestions[:3]}")
# If index matching failed, try order-based matching
if matched_by_index == 0 and len(data_items) > 0:
current_app.logger.warning(f"[BATCH] Index matching failed, trying order-based matching")
for i, item in enumerate(data_items):
if i < len(questions_data):
q = questions_data[i]
if q['image_id'] not in results:
suggestions = []
primary = item.get('chapter_title')
if primary and primary != 'Unclassified':
suggestions.append(primary)
if 'other_possible_chapters' in item:
others = item['other_possible_chapters']
if isinstance(others, list):
suggestions.extend([c for c in others if c and c != 'Unclassified'])
if not suggestions:
suggestions = fallback_topics.get(subject.lower(), ['Unclassified'])
results[q['image_id']] = {
'success': True,
'suggestions': suggestions[:5],
'subject': subject,
'other_possible_subjects': []
}
current_app.logger.info(f"[BATCH] Image {q['image_id']}: matched by order, suggestions={suggestions[:3]}")
# Fill in any missing results
for q in questions_data:
if q['image_id'] not in results:
current_app.logger.warning(f"[BATCH] Image {q['image_id']}: using fallback (no match in API response)")
results[q['image_id']] = {
'success': True,
'suggestions': fallback_topics.get(subject.lower(), ['Unclassified']),
'subject': subject,
'other_possible_subjects': []
}
return jsonify({'success': True, 'results': results})
except Exception as e:
# Return error but with fallback results for each image
fallback_result = {
'success': True,
'suggestions': ['Unclassified'],
'subject': subject,
'other_possible_subjects': [],
'error': str(e)
}
return jsonify({
'success': False,
'error': str(e),
'results': {img_id: fallback_result for img_id in image_ids}
})
@main_bp.route('/classified/update_single', methods=['POST'])
@login_required
def update_question_classification_single():
data = request.json
image_id, subject, chapter = data.get('image_id'), data.get('subject'), data.get('chapter')
if not image_id: return jsonify({'error': 'Image ID is required'}), 400
try:
conn = get_db_connection()
# Get image info and verify ownership
img_info = conn.execute("""
SELECT i.session_id, s.user_id FROM images i
JOIN sessions s ON i.session_id = s.id WHERE i.id = ?
""", (image_id,)).fetchone()
if not img_info or img_info['user_id'] != current_user.id:
conn.close(); return jsonify({'error': 'Unauthorized'}), 403
# Check if question row exists
existing = conn.execute('SELECT id FROM questions WHERE image_id = ?', (image_id,)).fetchone()
if existing:
conn.execute('UPDATE questions SET subject = ?, chapter = ? WHERE image_id = ?', (subject, chapter, image_id))
else:
# Create question row if it doesn't exist
conn.execute('''
INSERT INTO questions (session_id, image_id, question_number, subject, chapter, status)
VALUES (?, ?, '', ?, ?, 'unattempted')
''', (img_info['session_id'], image_id, subject, chapter))
# Auto-convert and sync neetprep collection
sync_neetprep_collection(conn, session_id, current_user.id)
conn.commit(); conn.close()
return jsonify({'success': True})
except Exception as e:
current_app.logger.error(f"Error updating classification: {e}")
return jsonify({'error': str(e)}), 500
@main_bp.route('/question_entry_v2/<session_id>')
@login_required
def question_entry_v2(session_id):
conn = get_db_connection()
session_data = conn.execute('SELECT original_filename, subject, tags, notes FROM sessions WHERE id = ? AND user_id = ?', (session_id, current_user.id)).fetchone()
if not session_data:
conn.close(); flash("Session not found or you don't have permission to access it.", "warning"); return redirect(url_for('dashboard.dashboard'))
images = conn.execute("""SELECT i.id, i.processed_filename, i.note_filename, i.note_json, i.include_note_in_pdf, q.question_number, q.status, q.marked_solution, q.actual_solution
FROM images i
LEFT JOIN questions q ON i.id = q.image_id
WHERE i.session_id = ? AND i.image_type = 'cropped'
ORDER BY i.id""", (session_id,)).fetchall()
classified_count = conn.execute("""SELECT COUNT(*) as count FROM images i LEFT JOIN questions q ON i.id = q.image_id WHERE i.session_id = ? AND i.image_type = 'cropped' AND q.subject IS NOT NULL AND q.chapter IS NOT NULL""", (session_id,)).fetchone()['count']
conn.close()
if not images: return "No questions were created from the PDF. Please go back and draw crop boxes.", 404
image_dicts = []
for img in images:
img_dict = dict(img)
note_payload = parse_note_payload(img_dict.get('note_json'))
linked_pages = note_payload.get('linked_pdf_pages', [])
img_dict['linked_pdf_pages'] = linked_pages
img_dict['linked_pdf_count'] = len(linked_pages)
img_dict['has_note'] = bool(img_dict.get('note_filename') or img_dict.get('note_json') or linked_pages)
image_dicts.append(img_dict)
return render_template('question_entry_v2.html', session_id=session_id, images=image_dicts, session_data=dict(session_data) if session_data else {}, classified_count=classified_count, total_questions=len(images), nvidia_nim_available=NVIDIA_NIM_AVAILABLE)
@main_bp.route(ROUTE_SAVE_QUESTIONS, methods=[METHOD_POST])
@login_required
def save_questions():
data = request.json
session_id, questions = data['session_id'], data['questions']
conn = get_db_connection()
session_owner = conn.execute('SELECT user_id FROM sessions WHERE id = ?', (session_id,)).fetchone()
if not session_owner or session_owner['user_id'] != current_user.id:
conn.close(); return jsonify({'error': 'Unauthorized'}), 403
conn.execute('UPDATE sessions SET subject = ?, tags = ?, notes = ? WHERE id = ?', (data.get('pdf_subject', ''), data.get('pdf_tags', ''), data.get('pdf_notes', ''), session_id))
# Use UPSERT to preserve subject and chapter data from classification
for q in questions:
existing = conn.execute('SELECT id, subject, chapter FROM questions WHERE image_id = ?', (q['image_id'],)).fetchone()
if existing:
# Update existing, preserve subject and chapter
conn.execute('''
UPDATE questions
SET question_number = ?, status = ?, marked_solution = ?, actual_solution = ?, tags = ?
WHERE image_id = ?
''', (q['question_number'], q['status'], q.get('marked_solution', ''), q.get('actual_solution', ''), data.get('pdf_tags', ''), q['image_id']))
else:
# Insert new
conn.execute('''
INSERT INTO questions (session_id, image_id, question_number, subject, status, marked_solution, actual_solution, time_taken, tags)
VALUES (?, ?, ?, '', ?, ?, ?, ?, ?)
''', (session_id, q['image_id'], q['question_number'], q['status'], q.get('marked_solution', ''), q.get('actual_solution', ''), q.get('time_taken', ''), data.get('pdf_tags', '')))
# Auto-convert and sync neetprep collection
sync_neetprep_collection(conn, session_id, current_user.id)
conn.commit(); conn.close()
return jsonify({'success': True, 'message': 'Questions saved successfully.'})
@main_bp.route('/autosave_question', methods=['POST'])
@login_required
def autosave_question():
"""Auto-save a single question's data without affecting other questions."""
data = request.json
session_id = data.get('session_id')
question = data.get('question', {})
if not session_id or not question.get('image_id'):
return jsonify({'error': 'Missing session_id or image_id'}), 400
conn = get_db_connection()
try:
# Verify session ownership
session_owner = conn.execute('SELECT user_id FROM sessions WHERE id = ?', (session_id,)).fetchone()
if not session_owner or session_owner['user_id'] != current_user.id:
conn.close()
return jsonify({'error': 'Unauthorized'}), 403
image_id = question['image_id']
question_number = question.get('question_number', '')
status = question.get('status', 'unattempted')
marked_solution = question.get('marked_solution', '')
actual_solution = question.get('actual_solution', '')
# Check if question already exists
existing = conn.execute('SELECT id, subject, chapter, tags FROM questions WHERE image_id = ?', (image_id,)).fetchone()
if existing:
# Update existing question, preserving subject and chapter
conn.execute('''
UPDATE questions
SET question_number = ?, status = ?, marked_solution = ?, actual_solution = ?
WHERE image_id = ?
''', (question_number, status, marked_solution, actual_solution, image_id))
else:
# Insert new question
conn.execute('''
INSERT INTO questions (session_id, image_id, question_number, subject, status, marked_solution, actual_solution, time_taken, tags)
VALUES (?, ?, ?, '', ?, ?, ?, '', '')
''', (session_id, image_id, question_number, status, marked_solution, actual_solution))
# Auto-convert and sync neetprep collection
sync_neetprep_collection(conn, session_id, current_user.id)
conn.commit()
conn.close()
return jsonify({'success': True})
except Exception as e:
conn.close()
current_app.logger.error(f"Auto-save question error: {e}")
return jsonify({'error': str(e)}), 500
@main_bp.route('/autosave_session_metadata', methods=['POST'])
@login_required
def autosave_session_metadata():
"""Auto-save session metadata (PDF subject, tags, notes)."""
data = request.json
session_id = data.get('session_id')
if not session_id:
return jsonify({'error': 'Missing session_id'}), 400
conn = get_db_connection()
try:
# Verify session ownership
session_owner = conn.execute('SELECT user_id FROM sessions WHERE id = ?', (session_id,)).fetchone()
if not session_owner or session_owner['user_id'] != current_user.id:
conn.close()
return jsonify({'error': 'Unauthorized'}), 403
# Update session metadata
conn.execute('''
UPDATE sessions
SET subject = ?, tags = ?, notes = ?
WHERE id = ?
''', (
data.get('pdf_subject', ''),
data.get('pdf_tags', ''),
data.get('pdf_notes', ''),
session_id
))
conn.commit()
conn.close()
return jsonify({'success': True})
except Exception as e:
conn.close()
current_app.logger.error(f"Auto-save metadata error: {e}")
return jsonify({'error': str(e)}), 500
@main_bp.route(ROUTE_EXTRACT_QUESTION_NUMBER, methods=[METHOD_POST])
@login_required
def extract_question_number():
if not NVIDIA_NIM_AVAILABLE: return jsonify({'error': 'NVIDIA NIM feature is not available.'}), 400
data = request.json
image_id = data.get('image_id')
if not image_id: return jsonify({'error': 'Missing image_id parameter'}), 400
try:
conn = get_db_connection()
image_owner = conn.execute("SELECT s.user_id FROM images i JOIN sessions s ON i.session_id = s.id WHERE i.id = ?", (image_id,)).fetchone()
if not image_owner or image_owner['user_id'] != current_user.id:
conn.close(); return jsonify({'error': 'Unauthorized'}), 403
image_info = conn.execute('SELECT processed_filename FROM images WHERE id = ?', (image_id,)).fetchone()
conn.close()
if not image_info or not image_info['processed_filename']: return jsonify({'error': 'Image not found or not processed'}), 404
image_path = os.path.join(current_app.config['PROCESSED_FOLDER'], image_info['processed_filename'])
if not os.path.exists(image_path): return jsonify({'error': 'Image file not found on disk'}), 404
image_bytes = resize_image_if_needed(image_path)
ocr_result = call_nim_ocr_api(image_bytes)
question_number = extract_question_number_from_ocr_result(ocr_result)
return jsonify({'success': True, 'question_number': question_number, 'image_id': image_id})
except Exception as e: return jsonify({'error': f'Failed to extract question number: {str(e)}'}), 500
@main_bp.route(ROUTE_EXTRACT_ALL_QUESTION_NUMBERS, methods=[METHOD_POST])
@login_required
def extract_all_question_numbers():
if not NVIDIA_NIM_AVAILABLE: return jsonify({'error': 'NVIDIA NIM feature is not available.'}), 400
data = request.json
session_id = data.get('session_id')
if not session_id: return jsonify({'error': 'Missing session_id parameter'}), 400
try:
conn = get_db_connection()
session_owner = conn.execute('SELECT user_id FROM sessions WHERE id = ?', (session_id,)).fetchone()
if not session_owner or session_owner['user_id'] != current_user.id:
conn.close(); return jsonify({'error': 'Unauthorized'}), 403
images = conn.execute("SELECT id, processed_filename FROM images WHERE session_id = ? AND image_type = 'cropped' ORDER BY id", (session_id,)).fetchall()
conn.close()
if not images: return jsonify({'error': 'No cropped images found in session'}), 404
results, errors = [], []
MAX_CONCURRENT_REQUESTS = 5
processed_count = 0
for image in images:
if processed_count >= MAX_CONCURRENT_REQUESTS:
import time; time.sleep(1); processed_count = 0
try:
image_id, processed_filename = image['id'], image['processed_filename']
if not processed_filename:
errors.append({'image_id': image_id, 'error': 'Image not processed'}); continue
image_path = os.path.join(current_app.config['PROCESSED_FOLDER'], processed_filename)
if not os.path.exists(image_path):
errors.append({'image_id': image_id, 'error': 'Image file not found on disk'}); continue
image_bytes = resize_image_if_needed(image_path)
ocr_result = call_nim_ocr_api(image_bytes)
question_number = extract_question_number_from_ocr_result(ocr_result)
results.append({'image_id': image_id, 'question_number': question_number})
processed_count += 1
except Exception as e: errors.append({'image_id': image['id'], 'error': str(e)})
return jsonify({'success': True, 'results': results, 'errors': errors})
except Exception as e: return jsonify({'error': f'Failed to extract question numbers: {str(e)}'}), 500
@main_bp.route('/delete_question/<image_id>', methods=['DELETE'])
@login_required
def delete_question(image_id):
try:
conn = get_db_connection()
image_owner = conn.execute("SELECT s.user_id FROM images i JOIN sessions s ON i.session_id = s.id WHERE i.id = ?", (image_id,)).fetchone()
if not image_owner or image_owner['user_id'] != current_user.id:
conn.close(); return jsonify({'error': 'Unauthorized'}), 403
image_info = conn.execute('SELECT session_id, filename, processed_filename FROM images WHERE id = ?', (image_id,)).fetchone()
if not image_info: conn.close(); return jsonify({'error': 'Question not found'}), 404
# Get question ID to delete bookmarks
q_row = conn.execute('SELECT id FROM questions WHERE image_id = ?', (image_id,)).fetchone()
if q_row:
conn.execute('DELETE FROM neetprep_bookmarks WHERE neetprep_question_id = ? AND question_type = ?', (str(q_row['id']), 'classified'))
conn.execute('DELETE FROM questions WHERE image_id = ?', (image_id,))
conn.execute('DELETE FROM images WHERE id = ?', (image_id,))
# Auto-convert and sync neetprep collection
sync_neetprep_collection(conn, image_info['session_id'], current_user.id)
conn.commit(); conn.close()
return jsonify({'success': True})
except Exception as e: return jsonify({'error': str(e)}), 500