Spaces:
Sleeping
Sleeping
| 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 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 | |
| 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) | |
| 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) | |
| }) | |
| 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} | |
| }) | |
| 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)) | |
| 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 | |
| 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.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 | |
| return render_template('question_entry_v2.html', session_id=session_id, images=[dict(img) for img in images], session_data=dict(session_data) if session_data else {}, classified_count=classified_count, total_questions=len(images), nvidia_nim_available=NVIDIA_NIM_AVAILABLE) | |
| 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', ''))) | |
| conn.commit(); conn.close() | |
| return jsonify({'success': True, 'message': 'Questions saved successfully.'}) | |
| 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)) | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| 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 | |
| conn.execute('DELETE FROM questions WHERE image_id = ?', (image_id,)) | |
| conn.execute('DELETE FROM images WHERE id = ?', (image_id,)) | |
| conn.commit(); conn.close() | |
| return jsonify({'success': True}) | |
| except Exception as e: return jsonify({'error': str(e)}), 500 |