From 6e8cf6f83ab6bf596c9db6fa30069a58819f068f Mon Sep 17 00:00:00 2001 From: Your Name <119736744+aborayan2022@users.noreply.github.com> Date: Mon, 30 Mar 2026 14:24:18 +0200 Subject: [PATCH] feat: harden PDF translation workflow --- .env.example | 5 + backend/app/routes/pdf_ai.py | 43 +- backend/app/routes/v1/tools.py | 364 +++- backend/app/services/pdf_ai_service.py | 472 +++++- backend/app/tasks/pdf_ai_tasks.py | 126 +- backend/config/__init__.py | 67 +- backend/tests/test_pdf_translate_service.py | 93 + frontend/public/sitemap.xml | 1490 +---------------- frontend/public/sitemaps/blog.xml | 10 +- frontend/public/sitemaps/seo.xml | 376 ++--- frontend/public/sitemaps/static.xml | 18 +- frontend/public/sitemaps/tools.xml | 88 +- .../src/components/tools/TranslatePdf.tsx | 106 +- frontend/src/i18n/ar.json | 9 +- frontend/src/i18n/en.json | 9 +- frontend/src/i18n/fr.json | 9 +- frontend/src/services/api.ts | 4 + 17 files changed, 1358 insertions(+), 1931 deletions(-) create mode 100644 backend/tests/test_pdf_translate_service.py diff --git a/.env.example b/.env.example index 018d569..bb74538 100644 --- a/.env.example +++ b/.env.example @@ -20,6 +20,11 @@ OPENROUTER_API_KEY= OPENROUTER_MODEL=nvidia/nemotron-3-super-120b-a12b:free OPENROUTER_BASE_URL=https://openrouter.ai/api/v1/chat/completions +# Premium document translation (recommended for Translate PDF) +DEEPL_API_KEY= +DEEPL_API_URL=https://api-free.deepl.com/v2/translate +DEEPL_TIMEOUT_SECONDS=90 + # AWS S3 AWS_ACCESS_KEY_ID= AWS_SECRET_ACCESS_KEY= diff --git a/backend/app/routes/pdf_ai.py b/backend/app/routes/pdf_ai.py index ba67875..0ef1da4 100644 --- a/backend/app/routes/pdf_ai.py +++ b/backend/app/routes/pdf_ai.py @@ -1,4 +1,5 @@ """PDF AI tool routes — Chat, Summarize, Translate, Table Extract.""" + from flask import Blueprint, request, jsonify from app.extensions import limiter @@ -70,10 +71,12 @@ def chat_pdf_route(): ) record_accepted_usage(actor, "chat-pdf", task.id) - return jsonify({ - "task_id": task.id, - "message": "Processing your question. Poll /api/tasks/{task_id}/status for progress.", - }), 202 + return jsonify( + { + "task_id": task.id, + "message": "Processing your question. Poll /api/tasks/{task_id}/status for progress.", + } + ), 202 # --------------------------------------------------------------------------- @@ -124,10 +127,12 @@ def summarize_pdf_route(): ) record_accepted_usage(actor, "summarize-pdf", task.id) - return jsonify({ - "task_id": task.id, - "message": "Summarizing document. Poll /api/tasks/{task_id}/status for progress.", - }), 202 + return jsonify( + { + "task_id": task.id, + "message": "Summarizing document. Poll /api/tasks/{task_id}/status for progress.", + } + ), 202 # --------------------------------------------------------------------------- @@ -149,6 +154,7 @@ def translate_pdf_route(): file = request.files["file"] target_language = request.form.get("target_language", "").strip() + source_language = request.form.get("source_language", "auto").strip() if not target_language: return jsonify({"error": "No target language specified."}), 400 @@ -174,14 +180,17 @@ def translate_pdf_route(): task_id, original_filename, target_language, + source_language, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "translate-pdf", task.id) - return jsonify({ - "task_id": task.id, - "message": "Translating document. Poll /api/tasks/{task_id}/status for progress.", - }), 202 + return jsonify( + { + "task_id": task.id, + "message": "Translating document. Poll /api/tasks/{task_id}/status for progress.", + } + ), 202 # --------------------------------------------------------------------------- @@ -226,7 +235,9 @@ def extract_tables_route(): ) record_accepted_usage(actor, "extract-tables", task.id) - return jsonify({ - "task_id": task.id, - "message": "Extracting tables. Poll /api/tasks/{task_id}/status for progress.", - }), 202 + return jsonify( + { + "task_id": task.id, + "message": "Extracting tables. Poll /api/tasks/{task_id}/status for progress.", + } + ), 202 diff --git a/backend/app/routes/v1/tools.py b/backend/app/routes/v1/tools.py index 907cd34..561bec5 100644 --- a/backend/app/routes/v1/tools.py +++ b/backend/app/routes/v1/tools.py @@ -1,4 +1,5 @@ """B2B API v1 tool routes — authenticated via X-API-Key, Pro plan only.""" + import os import uuid import logging @@ -37,16 +38,25 @@ from app.tasks.flowchart_tasks import extract_flowchart_task from app.tasks.ocr_tasks import ocr_image_task, ocr_pdf_task from app.tasks.removebg_tasks import remove_bg_task from app.tasks.pdf_ai_tasks import ( - chat_with_pdf_task, summarize_pdf_task, translate_pdf_task, extract_tables_task, + chat_with_pdf_task, + summarize_pdf_task, + translate_pdf_task, + extract_tables_task, ) from app.tasks.pdf_to_excel_tasks import pdf_to_excel_task from app.tasks.html_to_pdf_tasks import html_to_pdf_task from app.tasks.qrcode_tasks import generate_qr_task from app.tasks.pdf_convert_tasks import ( - pdf_to_pptx_task, excel_to_pdf_task, pptx_to_pdf_task, sign_pdf_task, + pdf_to_pptx_task, + excel_to_pdf_task, + pptx_to_pdf_task, + sign_pdf_task, ) from app.tasks.pdf_extra_tasks import ( - crop_pdf_task, flatten_pdf_task, repair_pdf_task, edit_metadata_task, + crop_pdf_task, + flatten_pdf_task, + repair_pdf_task, + edit_metadata_task, ) from app.tasks.image_extra_tasks import crop_image_task, rotate_flip_image_task from app.tasks.barcode_tasks import generate_barcode_task @@ -80,6 +90,7 @@ def _resolve_and_check() -> tuple: # Task status — GET /api/v1/tasks//status # --------------------------------------------------------------------------- + @v1_bp.route("/tasks//status", methods=["GET"]) @limiter.limit("300/minute", override_defaults=True) def get_task_status(task_id: str): @@ -113,6 +124,7 @@ def get_task_status(task_id: str): # Compress — POST /api/v1/compress/pdf # --------------------------------------------------------------------------- + @v1_bp.route("/compress/pdf", methods=["POST"]) @limiter.limit("10/minute") def compress_pdf_route(): @@ -130,7 +142,9 @@ def compress_pdf_route(): quality = "medium" try: - original_filename, ext = validate_actor_file(file, allowed_types=["pdf"], actor=actor) + original_filename, ext = validate_actor_file( + file, allowed_types=["pdf"], actor=actor + ) except FileValidationError as e: return jsonify({"error": e.message}), e.code @@ -138,7 +152,10 @@ def compress_pdf_route(): file.save(input_path) task = compress_pdf_task.delay( - input_path, task_id, original_filename, quality, + input_path, + task_id, + original_filename, + quality, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "compress-pdf", task.id) @@ -150,6 +167,7 @@ def compress_pdf_route(): # Convert — POST /api/v1/convert/pdf-to-word & /api/v1/convert/word-to-pdf # --------------------------------------------------------------------------- + @v1_bp.route("/convert/pdf-to-word", methods=["POST"]) @limiter.limit("10/minute") def pdf_to_word_route(): @@ -163,7 +181,9 @@ def pdf_to_word_route(): file = request.files["file"] try: - original_filename, ext = validate_actor_file(file, allowed_types=["pdf"], actor=actor) + original_filename, ext = validate_actor_file( + file, allowed_types=["pdf"], actor=actor + ) except FileValidationError as e: return jsonify({"error": e.message}), e.code @@ -171,7 +191,9 @@ def pdf_to_word_route(): file.save(input_path) task = convert_pdf_to_word.delay( - input_path, task_id, original_filename, + input_path, + task_id, + original_filename, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "pdf-to-word", task.id) @@ -201,7 +223,9 @@ def word_to_pdf_route(): file.save(input_path) task = convert_word_to_pdf.delay( - input_path, task_id, original_filename, + input_path, + task_id, + original_filename, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "word-to-pdf", task.id) @@ -212,6 +236,7 @@ def word_to_pdf_route(): # Image — POST /api/v1/image/convert & /api/v1/image/resize # --------------------------------------------------------------------------- + @v1_bp.route("/image/convert", methods=["POST"]) @limiter.limit("10/minute") def convert_image_route(): @@ -226,7 +251,9 @@ def convert_image_route(): file = request.files["file"] output_format = request.form.get("format", "").lower() if output_format not in ALLOWED_OUTPUT_FORMATS: - return jsonify({"error": f"Invalid format. Supported: {', '.join(ALLOWED_OUTPUT_FORMATS)}"}), 400 + return jsonify( + {"error": f"Invalid format. Supported: {', '.join(ALLOWED_OUTPUT_FORMATS)}"} + ), 400 try: quality = max(1, min(100, int(request.form.get("quality", "85")))) @@ -244,7 +271,11 @@ def convert_image_route(): file.save(input_path) task = convert_image_task.delay( - input_path, task_id, original_filename, output_format, quality, + input_path, + task_id, + original_filename, + output_format, + quality, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "image-convert", task.id) @@ -292,7 +323,12 @@ def resize_image_route(): file.save(input_path) task = resize_image_task.delay( - input_path, task_id, original_filename, width, height, quality, + input_path, + task_id, + original_filename, + width, + height, + quality, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "image-resize", task.id) @@ -303,6 +339,7 @@ def resize_image_route(): # Video — POST /api/v1/video/to-gif # --------------------------------------------------------------------------- + @v1_bp.route("/video/to-gif", methods=["POST"]) @limiter.limit("5/minute") def video_to_gif_route(): @@ -343,7 +380,13 @@ def video_to_gif_route(): file.save(input_path) task = create_gif_task.delay( - input_path, task_id, original_filename, start_time, duration, fps, width, + input_path, + task_id, + original_filename, + start_time, + duration, + fps, + width, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "video-to-gif", task.id) @@ -354,6 +397,7 @@ def video_to_gif_route(): # PDF Tools — all single-file and multi-file routes # --------------------------------------------------------------------------- + @v1_bp.route("/pdf-tools/merge", methods=["POST"]) @limiter.limit("10/minute") def merge_pdfs_route(): @@ -372,7 +416,9 @@ def merge_pdfs_route(): input_paths, original_filenames = [], [] for f in files: try: - original_filename, ext = validate_actor_file(f, allowed_types=["pdf"], actor=actor) + original_filename, ext = validate_actor_file( + f, allowed_types=["pdf"], actor=actor + ) except FileValidationError as e: return jsonify({"error": e.message}), e.code upload_dir = os.path.join(current_app.config["UPLOAD_FOLDER"], task_id) @@ -383,7 +429,9 @@ def merge_pdfs_route(): original_filenames.append(original_filename) task = merge_pdfs_task.delay( - input_paths, task_id, original_filenames, + input_paths, + task_id, + original_filenames, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "merge-pdf", task.id) @@ -410,14 +458,20 @@ def split_pdf_route(): return jsonify({"error": "Please specify which pages to extract."}), 400 try: - original_filename, ext = validate_actor_file(file, allowed_types=["pdf"], actor=actor) + original_filename, ext = validate_actor_file( + file, allowed_types=["pdf"], actor=actor + ) except FileValidationError as e: return jsonify({"error": e.message}), e.code task_id, input_path = generate_safe_path(ext, folder_type="upload") file.save(input_path) task = split_pdf_task.delay( - input_path, task_id, original_filename, mode, pages, + input_path, + task_id, + original_filename, + mode, + pages, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "split-pdf", task.id) @@ -445,14 +499,20 @@ def rotate_pdf_route(): pages = request.form.get("pages", "all") try: - original_filename, ext = validate_actor_file(file, allowed_types=["pdf"], actor=actor) + original_filename, ext = validate_actor_file( + file, allowed_types=["pdf"], actor=actor + ) except FileValidationError as e: return jsonify({"error": e.message}), e.code task_id, input_path = generate_safe_path(ext, folder_type="upload") file.save(input_path) task = rotate_pdf_task.delay( - input_path, task_id, original_filename, rotation, pages, + input_path, + task_id, + original_filename, + rotation, + pages, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "rotate-pdf", task.id) @@ -473,8 +533,12 @@ def add_page_numbers_route(): file = request.files["file"] position = request.form.get("position", "bottom-center") valid_positions = [ - "bottom-center", "bottom-right", "bottom-left", - "top-center", "top-right", "top-left", + "bottom-center", + "bottom-right", + "bottom-left", + "top-center", + "top-right", + "top-left", ] if position not in valid_positions: position = "bottom-center" @@ -484,14 +548,20 @@ def add_page_numbers_route(): start_number = 1 try: - original_filename, ext = validate_actor_file(file, allowed_types=["pdf"], actor=actor) + original_filename, ext = validate_actor_file( + file, allowed_types=["pdf"], actor=actor + ) except FileValidationError as e: return jsonify({"error": e.message}), e.code task_id, input_path = generate_safe_path(ext, folder_type="upload") file.save(input_path) task = add_page_numbers_task.delay( - input_path, task_id, original_filename, position, start_number, + input_path, + task_id, + original_filename, + position, + start_number, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "page-numbers", task.id) @@ -519,14 +589,20 @@ def pdf_to_images_route(): dpi = 200 try: - original_filename, ext = validate_actor_file(file, allowed_types=["pdf"], actor=actor) + original_filename, ext = validate_actor_file( + file, allowed_types=["pdf"], actor=actor + ) except FileValidationError as e: return jsonify({"error": e.message}), e.code task_id, input_path = generate_safe_path(ext, folder_type="upload") file.save(input_path) task = pdf_to_images_task.delay( - input_path, task_id, original_filename, output_format, dpi, + input_path, + task_id, + original_filename, + output_format, + dpi, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "pdf-to-images", task.id) @@ -564,7 +640,9 @@ def images_to_pdf_route(): original_filenames.append(original_filename) task = images_to_pdf_task.delay( - input_paths, task_id, original_filenames, + input_paths, + task_id, + original_filenames, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "images-to-pdf", task.id) @@ -594,14 +672,20 @@ def watermark_pdf_route(): opacity = 0.3 try: - original_filename, ext = validate_actor_file(file, allowed_types=["pdf"], actor=actor) + original_filename, ext = validate_actor_file( + file, allowed_types=["pdf"], actor=actor + ) except FileValidationError as e: return jsonify({"error": e.message}), e.code task_id, input_path = generate_safe_path(ext, folder_type="upload") file.save(input_path) task = watermark_pdf_task.delay( - input_path, task_id, original_filename, watermark_text, opacity, + input_path, + task_id, + original_filename, + watermark_text, + opacity, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "watermark-pdf", task.id) @@ -627,14 +711,19 @@ def protect_pdf_route(): return jsonify({"error": "Password must be at least 4 characters."}), 400 try: - original_filename, ext = validate_actor_file(file, allowed_types=["pdf"], actor=actor) + original_filename, ext = validate_actor_file( + file, allowed_types=["pdf"], actor=actor + ) except FileValidationError as e: return jsonify({"error": e.message}), e.code task_id, input_path = generate_safe_path(ext, folder_type="upload") file.save(input_path) task = protect_pdf_task.delay( - input_path, task_id, original_filename, password, + input_path, + task_id, + original_filename, + password, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "protect-pdf", task.id) @@ -658,14 +747,19 @@ def unlock_pdf_route(): return jsonify({"error": "Password is required."}), 400 try: - original_filename, ext = validate_actor_file(file, allowed_types=["pdf"], actor=actor) + original_filename, ext = validate_actor_file( + file, allowed_types=["pdf"], actor=actor + ) except FileValidationError as e: return jsonify({"error": e.message}), e.code task_id, input_path = generate_safe_path(ext, folder_type="upload") file.save(input_path) task = unlock_pdf_task.delay( - input_path, task_id, original_filename, password, + input_path, + task_id, + original_filename, + password, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "unlock-pdf", task.id) @@ -685,18 +779,24 @@ def extract_flowchart_route(): file = request.files["file"] try: - original_filename, ext = validate_actor_file(file, allowed_types=["pdf"], actor=actor) + original_filename, ext = validate_actor_file( + file, allowed_types=["pdf"], actor=actor + ) except FileValidationError as e: return jsonify({"error": e.message}), e.code task_id, input_path = generate_safe_path(ext) file.save(input_path) task = extract_flowchart_task.delay( - input_path, task_id, original_filename, + input_path, + task_id, + original_filename, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "pdf-flowchart", task.id) - return jsonify({"task_id": task.id, "message": "Flowchart extraction started."}), 202 + return jsonify( + {"task_id": task.id, "message": "Flowchart extraction started."} + ), 202 # =========================================================================== @@ -707,6 +807,7 @@ def extract_flowchart_route(): # OCR — POST /api/v1/ocr/image & /api/v1/ocr/pdf # --------------------------------------------------------------------------- + @v1_bp.route("/ocr/image", methods=["POST"]) @limiter.limit("10/minute") def ocr_image_route(): @@ -731,7 +832,10 @@ def ocr_image_route(): task_id, input_path = generate_safe_path(ext, folder_type="upload") file.save(input_path) task = ocr_image_task.delay( - input_path, task_id, original_filename, lang, + input_path, + task_id, + original_filename, + lang, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "ocr-image", task.id) @@ -753,14 +857,19 @@ def ocr_pdf_route(): lang = request.form.get("lang", "eng") try: - original_filename, ext = validate_actor_file(file, allowed_types=["pdf"], actor=actor) + original_filename, ext = validate_actor_file( + file, allowed_types=["pdf"], actor=actor + ) except FileValidationError as e: return jsonify({"error": e.message}), e.code task_id, input_path = generate_safe_path(ext, folder_type="upload") file.save(input_path) task = ocr_pdf_task.delay( - input_path, task_id, original_filename, lang, + input_path, + task_id, + original_filename, + lang, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "ocr-pdf", task.id) @@ -771,6 +880,7 @@ def ocr_pdf_route(): # Remove Background — POST /api/v1/image/remove-bg # --------------------------------------------------------------------------- + @v1_bp.route("/image/remove-bg", methods=["POST"]) @limiter.limit("5/minute") def remove_bg_route(): @@ -793,7 +903,9 @@ def remove_bg_route(): task_id, input_path = generate_safe_path(ext, folder_type="upload") file.save(input_path) task = remove_bg_task.delay( - input_path, task_id, original_filename, + input_path, + task_id, + original_filename, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "remove-bg", task.id) @@ -804,6 +916,7 @@ def remove_bg_route(): # PDF AI — POST /api/v1/pdf-ai/chat, summarize, translate, extract-tables # --------------------------------------------------------------------------- + @v1_bp.route("/pdf-ai/chat", methods=["POST"]) @limiter.limit("5/minute") def chat_pdf_route(): @@ -821,14 +934,19 @@ def chat_pdf_route(): return jsonify({"error": "Question is required."}), 400 try: - original_filename, ext = validate_actor_file(file, allowed_types=["pdf"], actor=actor) + original_filename, ext = validate_actor_file( + file, allowed_types=["pdf"], actor=actor + ) except FileValidationError as e: return jsonify({"error": e.message}), e.code task_id, input_path = generate_safe_path(ext, folder_type="upload") file.save(input_path) task = chat_with_pdf_task.delay( - input_path, task_id, original_filename, question, + input_path, + task_id, + original_filename, + question, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "chat-pdf", task.id) @@ -852,14 +970,19 @@ def summarize_pdf_route(): length = "medium" try: - original_filename, ext = validate_actor_file(file, allowed_types=["pdf"], actor=actor) + original_filename, ext = validate_actor_file( + file, allowed_types=["pdf"], actor=actor + ) except FileValidationError as e: return jsonify({"error": e.message}), e.code task_id, input_path = generate_safe_path(ext, folder_type="upload") file.save(input_path) task = summarize_pdf_task.delay( - input_path, task_id, original_filename, length, + input_path, + task_id, + original_filename, + length, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "summarize-pdf", task.id) @@ -879,18 +1002,25 @@ def translate_pdf_route(): file = request.files["file"] target_language = request.form.get("target_language", "").strip() + source_language = request.form.get("source_language", "auto").strip() if not target_language: return jsonify({"error": "Target language is required."}), 400 try: - original_filename, ext = validate_actor_file(file, allowed_types=["pdf"], actor=actor) + original_filename, ext = validate_actor_file( + file, allowed_types=["pdf"], actor=actor + ) except FileValidationError as e: return jsonify({"error": e.message}), e.code task_id, input_path = generate_safe_path(ext, folder_type="upload") file.save(input_path) task = translate_pdf_task.delay( - input_path, task_id, original_filename, target_language, + input_path, + task_id, + original_filename, + target_language, + source_language, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "translate-pdf", task.id) @@ -910,14 +1040,18 @@ def extract_tables_route(): file = request.files["file"] try: - original_filename, ext = validate_actor_file(file, allowed_types=["pdf"], actor=actor) + original_filename, ext = validate_actor_file( + file, allowed_types=["pdf"], actor=actor + ) except FileValidationError as e: return jsonify({"error": e.message}), e.code task_id, input_path = generate_safe_path(ext, folder_type="upload") file.save(input_path) task = extract_tables_task.delay( - input_path, task_id, original_filename, + input_path, + task_id, + original_filename, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "extract-tables", task.id) @@ -928,6 +1062,7 @@ def extract_tables_route(): # PDF to Excel — POST /api/v1/convert/pdf-to-excel # --------------------------------------------------------------------------- + @v1_bp.route("/convert/pdf-to-excel", methods=["POST"]) @limiter.limit("10/minute") def pdf_to_excel_route(): @@ -941,14 +1076,18 @@ def pdf_to_excel_route(): file = request.files["file"] try: - original_filename, ext = validate_actor_file(file, allowed_types=["pdf"], actor=actor) + original_filename, ext = validate_actor_file( + file, allowed_types=["pdf"], actor=actor + ) except FileValidationError as e: return jsonify({"error": e.message}), e.code task_id, input_path = generate_safe_path(ext, folder_type="upload") file.save(input_path) task = pdf_to_excel_task.delay( - input_path, task_id, original_filename, + input_path, + task_id, + original_filename, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "pdf-to-excel", task.id) @@ -959,6 +1098,7 @@ def pdf_to_excel_route(): # HTML to PDF — POST /api/v1/convert/html-to-pdf # --------------------------------------------------------------------------- + @v1_bp.route("/convert/html-to-pdf", methods=["POST"]) @limiter.limit("10/minute") def html_to_pdf_route(): @@ -981,7 +1121,9 @@ def html_to_pdf_route(): task_id, input_path = generate_safe_path(ext, folder_type="upload") file.save(input_path) task = html_to_pdf_task.delay( - input_path, task_id, original_filename, + input_path, + task_id, + original_filename, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "html-to-pdf", task.id) @@ -992,6 +1134,7 @@ def html_to_pdf_route(): # QR Code — POST /api/v1/qrcode/generate # --------------------------------------------------------------------------- + @v1_bp.route("/qrcode/generate", methods=["POST"]) @limiter.limit("20/minute") def generate_qr_route(): @@ -1018,7 +1161,10 @@ def generate_qr_route(): task_id = str(uuid.uuid4()) task = generate_qr_task.delay( - task_id, str(data).strip(), size, "png", + task_id, + str(data).strip(), + size, + "png", **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "qr-code", task.id) @@ -1033,6 +1179,7 @@ def generate_qr_route(): # PDF to PowerPoint — POST /api/v1/convert/pdf-to-pptx # --------------------------------------------------------------------------- + @v1_bp.route("/convert/pdf-to-pptx", methods=["POST"]) @limiter.limit("10/minute") def v1_pdf_to_pptx_route(): @@ -1046,14 +1193,18 @@ def v1_pdf_to_pptx_route(): file = request.files["file"] try: - original_filename, ext = validate_actor_file(file, allowed_types=["pdf"], actor=actor) + original_filename, ext = validate_actor_file( + file, allowed_types=["pdf"], actor=actor + ) except FileValidationError as e: return jsonify({"error": e.message}), e.code task_id, input_path = generate_safe_path(ext, folder_type="upload") file.save(input_path) task = pdf_to_pptx_task.delay( - input_path, task_id, original_filename, + input_path, + task_id, + original_filename, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "pdf-to-pptx", task.id) @@ -1064,6 +1215,7 @@ def v1_pdf_to_pptx_route(): # Excel to PDF — POST /api/v1/convert/excel-to-pdf # --------------------------------------------------------------------------- + @v1_bp.route("/convert/excel-to-pdf", methods=["POST"]) @limiter.limit("10/minute") def v1_excel_to_pdf_route(): @@ -1086,7 +1238,9 @@ def v1_excel_to_pdf_route(): task_id, input_path = generate_safe_path(ext, folder_type="upload") file.save(input_path) task = excel_to_pdf_task.delay( - input_path, task_id, original_filename, + input_path, + task_id, + original_filename, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "excel-to-pdf", task.id) @@ -1097,6 +1251,7 @@ def v1_excel_to_pdf_route(): # PowerPoint to PDF — POST /api/v1/convert/pptx-to-pdf # --------------------------------------------------------------------------- + @v1_bp.route("/convert/pptx-to-pdf", methods=["POST"]) @limiter.limit("10/minute") def v1_pptx_to_pdf_route(): @@ -1119,7 +1274,9 @@ def v1_pptx_to_pdf_route(): task_id, input_path = generate_safe_path(ext, folder_type="upload") file.save(input_path) task = pptx_to_pdf_task.delay( - input_path, task_id, original_filename, + input_path, + task_id, + original_filename, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "pptx-to-pdf", task.id) @@ -1130,6 +1287,7 @@ def v1_pptx_to_pdf_route(): # Sign PDF — POST /api/v1/pdf-tools/sign # --------------------------------------------------------------------------- + @v1_bp.route("/pdf-tools/sign", methods=["POST"]) @limiter.limit("10/minute") def v1_sign_pdf_route(): @@ -1147,12 +1305,16 @@ def v1_sign_pdf_route(): sig_file = request.files["signature"] try: - original_filename, ext = validate_actor_file(pdf_file, allowed_types=["pdf"], actor=actor) + original_filename, ext = validate_actor_file( + pdf_file, allowed_types=["pdf"], actor=actor + ) except FileValidationError as e: return jsonify({"error": e.message}), e.code try: - _, sig_ext = validate_actor_file(sig_file, allowed_types=ALLOWED_IMAGE_TYPES, actor=actor) + _, sig_ext = validate_actor_file( + sig_file, allowed_types=ALLOWED_IMAGE_TYPES, actor=actor + ) except FileValidationError as e: return jsonify({"error": f"Signature: {e.message}"}), e.code @@ -1174,8 +1336,15 @@ def v1_sign_pdf_route(): sig_file.save(signature_path) task = sign_pdf_task.delay( - input_path, signature_path, task_id, original_filename, - page, x, y, width, height, + input_path, + signature_path, + task_id, + original_filename, + page, + x, + y, + width, + height, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "sign-pdf", task.id) @@ -1186,6 +1355,7 @@ def v1_sign_pdf_route(): # Crop PDF — POST /api/v1/pdf-tools/crop # --------------------------------------------------------------------------- + @v1_bp.route("/pdf-tools/crop", methods=["POST"]) @limiter.limit("10/minute") def v1_crop_pdf_route(): @@ -1209,15 +1379,23 @@ def v1_crop_pdf_route(): pages = request.form.get("pages", "all") try: - original_filename, ext = validate_actor_file(file, allowed_types=["pdf"], actor=actor) + original_filename, ext = validate_actor_file( + file, allowed_types=["pdf"], actor=actor + ) except FileValidationError as e: return jsonify({"error": e.message}), e.code task_id, input_path = generate_safe_path(ext, folder_type="upload") file.save(input_path) task = crop_pdf_task.delay( - input_path, task_id, original_filename, - margin_left, margin_right, margin_top, margin_bottom, pages, + input_path, + task_id, + original_filename, + margin_left, + margin_right, + margin_top, + margin_bottom, + pages, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "crop-pdf", task.id) @@ -1228,6 +1406,7 @@ def v1_crop_pdf_route(): # Flatten PDF — POST /api/v1/pdf-tools/flatten # --------------------------------------------------------------------------- + @v1_bp.route("/pdf-tools/flatten", methods=["POST"]) @limiter.limit("10/minute") def v1_flatten_pdf_route(): @@ -1241,14 +1420,18 @@ def v1_flatten_pdf_route(): file = request.files["file"] try: - original_filename, ext = validate_actor_file(file, allowed_types=["pdf"], actor=actor) + original_filename, ext = validate_actor_file( + file, allowed_types=["pdf"], actor=actor + ) except FileValidationError as e: return jsonify({"error": e.message}), e.code task_id, input_path = generate_safe_path(ext, folder_type="upload") file.save(input_path) task = flatten_pdf_task.delay( - input_path, task_id, original_filename, + input_path, + task_id, + original_filename, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "flatten-pdf", task.id) @@ -1259,6 +1442,7 @@ def v1_flatten_pdf_route(): # Repair PDF — POST /api/v1/pdf-tools/repair # --------------------------------------------------------------------------- + @v1_bp.route("/pdf-tools/repair", methods=["POST"]) @limiter.limit("10/minute") def v1_repair_pdf_route(): @@ -1272,14 +1456,18 @@ def v1_repair_pdf_route(): file = request.files["file"] try: - original_filename, ext = validate_actor_file(file, allowed_types=["pdf"], actor=actor) + original_filename, ext = validate_actor_file( + file, allowed_types=["pdf"], actor=actor + ) except FileValidationError as e: return jsonify({"error": e.message}), e.code task_id, input_path = generate_safe_path(ext, folder_type="upload") file.save(input_path) task = repair_pdf_task.delay( - input_path, task_id, original_filename, + input_path, + task_id, + original_filename, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "repair-pdf", task.id) @@ -1290,6 +1478,7 @@ def v1_repair_pdf_route(): # Edit PDF Metadata — POST /api/v1/pdf-tools/metadata # --------------------------------------------------------------------------- + @v1_bp.route("/pdf-tools/metadata", methods=["POST"]) @limiter.limit("10/minute") def v1_edit_metadata_route(): @@ -1312,15 +1501,23 @@ def v1_edit_metadata_route(): return jsonify({"error": "At least one metadata field required."}), 400 try: - original_filename, ext = validate_actor_file(file, allowed_types=["pdf"], actor=actor) + original_filename, ext = validate_actor_file( + file, allowed_types=["pdf"], actor=actor + ) except FileValidationError as e: return jsonify({"error": e.message}), e.code task_id, input_path = generate_safe_path(ext, folder_type="upload") file.save(input_path) task = edit_metadata_task.delay( - input_path, task_id, original_filename, - title, author, subject, keywords, creator, + input_path, + task_id, + original_filename, + title, + author, + subject, + keywords, + creator, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "edit-metadata", task.id) @@ -1331,6 +1528,7 @@ def v1_edit_metadata_route(): # Image Crop — POST /api/v1/image/crop # --------------------------------------------------------------------------- + @v1_bp.route("/image/crop", methods=["POST"]) @limiter.limit("10/minute") def v1_crop_image_route(): @@ -1364,8 +1562,13 @@ def v1_crop_image_route(): task_id, input_path = generate_safe_path(ext, folder_type="upload") file.save(input_path) task = crop_image_task.delay( - input_path, task_id, original_filename, - left, top, right, bottom, + input_path, + task_id, + original_filename, + left, + top, + right, + bottom, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "image-crop", task.id) @@ -1376,6 +1579,7 @@ def v1_crop_image_route(): # Image Rotate/Flip — POST /api/v1/image/rotate-flip # --------------------------------------------------------------------------- + @v1_bp.route("/image/rotate-flip", methods=["POST"]) @limiter.limit("10/minute") def v1_rotate_flip_image_route(): @@ -1408,8 +1612,12 @@ def v1_rotate_flip_image_route(): task_id, input_path = generate_safe_path(ext, folder_type="upload") file.save(input_path) task = rotate_flip_image_task.delay( - input_path, task_id, original_filename, - rotation, flip_horizontal, flip_vertical, + input_path, + task_id, + original_filename, + rotation, + flip_horizontal, + flip_vertical, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "image-rotate-flip", task.id) @@ -1420,6 +1628,7 @@ def v1_rotate_flip_image_route(): # Barcode — POST /api/v1/barcode/generate # --------------------------------------------------------------------------- + @v1_bp.route("/barcode/generate", methods=["POST"]) @limiter.limit("20/minute") def v1_generate_barcode_route(): @@ -1442,14 +1651,21 @@ def v1_generate_barcode_route(): return jsonify({"error": "Barcode data is required."}), 400 if barcode_type not in SUPPORTED_BARCODE_TYPES: - return jsonify({"error": f"Unsupported type. Supported: {', '.join(SUPPORTED_BARCODE_TYPES)}"}), 400 + return jsonify( + { + "error": f"Unsupported type. Supported: {', '.join(SUPPORTED_BARCODE_TYPES)}" + } + ), 400 if output_format not in ("png", "svg"): output_format = "png" task_id = str(uuid.uuid4()) task = generate_barcode_task.delay( - data, barcode_type, task_id, output_format, + data, + barcode_type, + task_id, + output_format, **build_task_tracking_kwargs(actor), ) record_accepted_usage(actor, "barcode", task.id) diff --git a/backend/app/services/pdf_ai_service.py b/backend/app/services/pdf_ai_service.py index 655798c..a0891f6 100644 --- a/backend/app/services/pdf_ai_service.py +++ b/backend/app/services/pdf_ai_service.py @@ -1,6 +1,11 @@ """PDF AI services — Chat, Summarize, Translate, Table Extract.""" + import json import logging +import os +import tempfile +import time +from dataclasses import dataclass import requests @@ -11,9 +16,84 @@ from app.services.openrouter_config_service import ( logger = logging.getLogger(__name__) +DEFAULT_DEEPL_API_URL = "https://api-free.deepl.com/v2/translate" +DEFAULT_DEEPL_TIMEOUT_SECONDS = 90 +MAX_TRANSLATION_CHUNK_CHARS = 3500 +TRANSLATION_RETRY_ATTEMPTS = 3 +TRANSLATION_RETRY_DELAY_SECONDS = 2 + +LANGUAGE_LABELS = { + "auto": "Auto Detect", + "en": "English", + "ar": "Arabic", + "fr": "French", + "es": "Spanish", + "de": "German", + "zh": "Chinese", + "ja": "Japanese", + "ko": "Korean", + "pt": "Portuguese", + "ru": "Russian", + "tr": "Turkish", + "it": "Italian", +} + +DEEPL_LANGUAGE_CODES = { + "ar": "AR", + "de": "DE", + "en": "EN", + "es": "ES", + "fr": "FR", + "it": "IT", + "ja": "JA", + "ko": "KO", + "pt": "PT-PT", + "ru": "RU", + "tr": "TR", + "zh": "ZH", +} + +OCR_LANGUAGE_CODES = { + "ar": "ara", + "en": "eng", + "fr": "fra", +} + + +@dataclass(frozen=True) +class DeepLSettings: + api_key: str + base_url: str + timeout_seconds: int + + +def _normalize_language_code(value: str | None, default: str = "") -> str: + normalized = str(value or "").strip().lower() + return normalized or default + + +def _language_label(value: str | None) -> str: + normalized = _normalize_language_code(value) + return LANGUAGE_LABELS.get(normalized, normalized or "Unknown") + + +def _get_deepl_settings() -> DeepLSettings: + api_key = str(os.getenv("DEEPL_API_KEY", "")).strip() + base_url = ( + str(os.getenv("DEEPL_API_URL", DEFAULT_DEEPL_API_URL)).strip() + or DEFAULT_DEEPL_API_URL + ) + timeout_seconds = int( + os.getenv("DEEPL_TIMEOUT_SECONDS", DEFAULT_DEEPL_TIMEOUT_SECONDS) + ) + return DeepLSettings( + api_key=api_key, base_url=base_url, timeout_seconds=timeout_seconds + ) + class PdfAiError(Exception): """Custom exception for PDF AI service failures.""" + def __init__( self, user_message: str, @@ -26,6 +106,42 @@ class PdfAiError(Exception): self.detail = detail +class RetryableTranslationError(PdfAiError): + """Error wrapper used for provider failures that should be retried.""" + + +def _translate_with_retry(action, provider_name: str) -> dict: + last_error: PdfAiError | None = None + + for attempt in range(1, TRANSLATION_RETRY_ATTEMPTS + 1): + try: + return action() + except RetryableTranslationError as error: + last_error = error + logger.warning( + "%s translation attempt %s/%s failed with retryable error %s", + provider_name, + attempt, + TRANSLATION_RETRY_ATTEMPTS, + error.error_code, + ) + if attempt == TRANSLATION_RETRY_ATTEMPTS: + break + time.sleep(TRANSLATION_RETRY_DELAY_SECONDS * attempt) + + if last_error: + raise PdfAiError( + last_error.user_message, + error_code=last_error.error_code, + detail=last_error.detail, + ) + + raise PdfAiError( + "Translation provider failed unexpectedly.", + error_code="TRANSLATION_PROVIDER_FAILED", + ) + + def _estimate_tokens(text: str) -> int: """Rough token estimate: ~4 chars per token for English.""" return max(1, len(text) // 4) @@ -49,7 +165,30 @@ def _extract_text_from_pdf(input_path: str, max_pages: int = 50) -> str: text = page.extract_text() or "" if text.strip(): texts.append(f"[Page {i + 1}]\n{text}") - return "\n\n".join(texts) + + extracted = "\n\n".join(texts) + if extracted.strip(): + return extracted + + # Fall back to OCR for scanned/image-only PDFs instead of failing fast. + try: + from app.services.ocr_service import ocr_pdf + + with tempfile.NamedTemporaryFile(suffix=".txt", delete=False) as handle: + ocr_output_path = handle.name + + try: + data = ocr_pdf(input_path, ocr_output_path, lang="eng") + ocr_text = str(data.get("text", "")).strip() + if ocr_text: + return ocr_text + finally: + if os.path.exists(ocr_output_path): + os.unlink(ocr_output_path) + except Exception as ocr_error: + logger.warning("OCR fallback for PDF text extraction failed: %s", ocr_error) + + return "" except PdfAiError: raise except Exception as e: @@ -70,14 +209,17 @@ def _call_openrouter( # Budget guard try: from app.services.ai_cost_service import check_ai_budget, AiBudgetExceededError + check_ai_budget() - except AiBudgetExceededError: - raise PdfAiError( - "Monthly AI processing budget has been reached. Please try again next month.", - error_code="AI_BUDGET_EXCEEDED", - ) - except Exception: - pass # Don't block if cost service unavailable + except ImportError: + pass + except Exception as error: + if error.__class__.__name__ == "AiBudgetExceededError": + raise PdfAiError( + "Monthly AI processing budget has been reached. Please try again next month.", + error_code="AI_BUDGET_EXCEEDED", + ) + pass settings = get_openrouter_settings() @@ -127,14 +269,14 @@ def _call_openrouter( if status_code == 429: logger.warning("OpenRouter rate limit reached (429).") - raise PdfAiError( + raise RetryableTranslationError( "AI service is experiencing high demand. Please wait a moment and try again.", error_code="OPENROUTER_RATE_LIMIT", ) if status_code >= 500: logger.error("OpenRouter server error (%s).", status_code) - raise PdfAiError( + raise RetryableTranslationError( "AI service provider is experiencing issues. Please try again shortly.", error_code="OPENROUTER_SERVER_ERROR", ) @@ -144,7 +286,11 @@ def _call_openrouter( # Handle model-level errors returned inside a 200 response if data.get("error"): - error_msg = data["error"].get("message", "") if isinstance(data["error"], dict) else str(data["error"]) + error_msg = ( + data["error"].get("message", "") + if isinstance(data["error"], dict) + else str(data["error"]) + ) logger.error("OpenRouter returned an error payload: %s", error_msg) raise PdfAiError( "AI service encountered an issue. Please try again.", @@ -163,6 +309,7 @@ def _call_openrouter( # Log usage try: from app.services.ai_cost_service import log_ai_usage + usage = data.get("usage", {}) log_ai_usage( tool=tool_name, @@ -178,13 +325,13 @@ def _call_openrouter( except PdfAiError: raise except requests.exceptions.Timeout: - raise PdfAiError( + raise RetryableTranslationError( "AI service timed out. Please try again.", error_code="OPENROUTER_TIMEOUT", ) except requests.exceptions.ConnectionError: logger.error("Cannot connect to OpenRouter API at %s", settings.base_url) - raise PdfAiError( + raise RetryableTranslationError( "AI service is unreachable. Please try again shortly.", error_code="OPENROUTER_CONNECTION_ERROR", ) @@ -197,6 +344,218 @@ def _call_openrouter( ) +def _split_translation_chunks( + text: str, max_chars: int = MAX_TRANSLATION_CHUNK_CHARS +) -> list[str]: + """Split extracted PDF text into stable chunks while preserving page markers.""" + chunks: list[str] = [] + current: list[str] = [] + current_length = 0 + + for block in text.split("\n\n"): + normalized = block.strip() + if not normalized: + continue + + block_length = len(normalized) + 2 + if current and current_length + block_length > max_chars: + chunks.append("\n\n".join(current)) + current = [normalized] + current_length = block_length + continue + + current.append(normalized) + current_length += block_length + + if current: + chunks.append("\n\n".join(current)) + + return chunks or [text] + + +def _call_deepl_translate( + chunk: str, target_language: str, source_language: str | None = None +) -> dict: + """Translate a chunk with DeepL when premium credentials are configured.""" + settings = _get_deepl_settings() + if not settings.api_key: + raise PdfAiError( + "DeepL is not configured.", + error_code="DEEPL_NOT_CONFIGURED", + ) + + target_code = DEEPL_LANGUAGE_CODES.get(_normalize_language_code(target_language)) + if not target_code: + raise PdfAiError( + f"Target language '{target_language}' is not supported by the premium translation provider.", + error_code="DEEPL_UNSUPPORTED_TARGET_LANGUAGE", + ) + + payload: dict[str, object] = { + "text": [chunk], + "target_lang": target_code, + "preserve_formatting": True, + "tag_handling": "xml", + "split_sentences": "nonewlines", + } + + source_code = DEEPL_LANGUAGE_CODES.get(_normalize_language_code(source_language)) + if source_code: + payload["source_lang"] = source_code + + try: + response = requests.post( + settings.base_url, + headers={ + "Authorization": f"DeepL-Auth-Key {settings.api_key}", + "Content-Type": "application/json", + }, + json=payload, + timeout=settings.timeout_seconds, + ) + except requests.exceptions.Timeout: + raise RetryableTranslationError( + "Premium translation service timed out. Retrying...", + error_code="DEEPL_TIMEOUT", + ) + except requests.exceptions.ConnectionError: + raise RetryableTranslationError( + "Premium translation service is temporarily unreachable. Retrying...", + error_code="DEEPL_CONNECTION_ERROR", + ) + except requests.exceptions.RequestException as error: + raise PdfAiError( + "Premium translation service is temporarily unavailable.", + error_code="DEEPL_REQUEST_ERROR", + detail=str(error), + ) + + if response.status_code == 429: + raise RetryableTranslationError( + "Premium translation service is busy. Retrying...", + error_code="DEEPL_RATE_LIMIT", + ) + + if response.status_code >= 500: + raise RetryableTranslationError( + "Premium translation service is experiencing issues. Retrying...", + error_code="DEEPL_SERVER_ERROR", + ) + + if response.status_code in {403, 456}: + raise PdfAiError( + "Premium translation provider credits or permissions need attention.", + error_code="DEEPL_CREDITS_OR_PERMISSIONS", + ) + + response.raise_for_status() + data = response.json() + translations = data.get("translations") or [] + if not translations: + raise PdfAiError( + "Premium translation provider returned an empty response.", + error_code="DEEPL_EMPTY_RESPONSE", + ) + + first = translations[0] + translated_text = str(first.get("text", "")).strip() + if not translated_text: + raise PdfAiError( + "Premium translation provider returned an empty response.", + error_code="DEEPL_EMPTY_TEXT", + ) + + return { + "translation": translated_text, + "provider": "deepl", + "detected_source_language": str(first.get("detected_source_language", "")) + .strip() + .lower(), + } + + +def _call_openrouter_translate( + chunk: str, target_language: str, source_language: str | None = None +) -> dict: + source_hint = "auto-detect the source language" + if source_language and _normalize_language_code(source_language) != "auto": + source_hint = f"treat {_language_label(source_language)} as the source language" + + system_prompt = ( + "You are a professional document translator. " + f"Translate the provided PDF content into {_language_label(target_language)}. " + f"Please {source_hint}. Preserve headings, lists, tables, and page markers. " + "Return only the translated text." + ) + translation = _call_openrouter( + system_prompt, + chunk, + max_tokens=2200, + tool_name="pdf_translate_fallback", + ) + return { + "translation": translation, + "provider": "openrouter", + "detected_source_language": _normalize_language_code( + source_language, default="" + ), + } + + +def _translate_document_text( + text: str, target_language: str, source_language: str | None = None +) -> dict: + chunks = _split_translation_chunks(text) + translations: list[str] = [] + detected_source_language = _normalize_language_code(source_language) + if detected_source_language == "auto": + detected_source_language = "" + providers_used: list[str] = [] + + for chunk in chunks: + chunk_result: dict | None = None + + deepl_settings = _get_deepl_settings() + if deepl_settings.api_key: + try: + chunk_result = _translate_with_retry( + lambda: _call_deepl_translate( + chunk, target_language, source_language + ), + provider_name="DeepL", + ) + except PdfAiError as deepl_error: + logger.warning( + "DeepL translation failed for chunk; falling back to OpenRouter. code=%s detail=%s", + deepl_error.error_code, + deepl_error.detail, + ) + + if chunk_result is None: + chunk_result = _translate_with_retry( + lambda: _call_openrouter_translate( + chunk, target_language, source_language + ), + provider_name="OpenRouter", + ) + + translations.append(str(chunk_result["translation"]).strip()) + providers_used.append(str(chunk_result["provider"])) + if not detected_source_language and chunk_result.get( + "detected_source_language" + ): + detected_source_language = _normalize_language_code( + chunk_result["detected_source_language"] + ) + + return { + "translation": "\n\n".join(part for part in translations if part), + "provider": ", ".join(sorted(set(providers_used))), + "detected_source_language": detected_source_language, + "chunks_translated": len(translations), + } + + # --------------------------------------------------------------------------- # 1. Chat with PDF # --------------------------------------------------------------------------- @@ -212,11 +571,15 @@ def chat_with_pdf(input_path: str, question: str) -> dict: {"reply": "...", "pages_analyzed": int} """ if not question or not question.strip(): - raise PdfAiError("Please provide a question.", error_code="PDF_AI_INVALID_INPUT") + raise PdfAiError( + "Please provide a question.", error_code="PDF_AI_INVALID_INPUT" + ) text = _extract_text_from_pdf(input_path) if not text.strip(): - raise PdfAiError("Could not extract any text from the PDF.", error_code="PDF_TEXT_EMPTY") + raise PdfAiError( + "Could not extract any text from the PDF.", error_code="PDF_TEXT_EMPTY" + ) # Truncate to fit context window max_chars = 12000 @@ -230,7 +593,9 @@ def chat_with_pdf(input_path: str, question: str) -> dict: ) user_msg = f"Document content:\n{truncated}\n\nQuestion: {question}" - reply = _call_openrouter(system_prompt, user_msg, max_tokens=800, tool_name="pdf_chat") + reply = _call_openrouter( + system_prompt, user_msg, max_tokens=800, tool_name="pdf_chat" + ) page_count = text.count("[Page ") return {"reply": reply, "pages_analyzed": page_count} @@ -252,7 +617,9 @@ def summarize_pdf(input_path: str, length: str = "medium") -> dict: """ text = _extract_text_from_pdf(input_path) if not text.strip(): - raise PdfAiError("Could not extract any text from the PDF.", error_code="PDF_TEXT_EMPTY") + raise PdfAiError( + "Could not extract any text from the PDF.", error_code="PDF_TEXT_EMPTY" + ) length_instruction = { "short": "Provide a brief summary in 2-3 sentences.", @@ -270,7 +637,9 @@ def summarize_pdf(input_path: str, length: str = "medium") -> dict: ) user_msg = f"{length_instruction}\n\nDocument content:\n{truncated}" - summary = _call_openrouter(system_prompt, user_msg, max_tokens=1000, tool_name="pdf_summarize") + summary = _call_openrouter( + system_prompt, user_msg, max_tokens=1000, tool_name="pdf_summarize" + ) page_count = text.count("[Page ") return {"summary": summary, "pages_analyzed": page_count} @@ -279,7 +648,9 @@ def summarize_pdf(input_path: str, length: str = "medium") -> dict: # --------------------------------------------------------------------------- # 3. Translate PDF # --------------------------------------------------------------------------- -def translate_pdf(input_path: str, target_language: str) -> dict: +def translate_pdf( + input_path: str, target_language: str, source_language: str | None = None +) -> dict: """ Translate the text content of a PDF to another language. @@ -290,29 +661,46 @@ def translate_pdf(input_path: str, target_language: str) -> dict: Returns: {"translation": "...", "pages_analyzed": int, "target_language": str} """ - if not target_language or not target_language.strip(): - raise PdfAiError("Please specify a target language.", error_code="PDF_AI_INVALID_INPUT") + normalized_target_language = _normalize_language_code(target_language) + normalized_source_language = _normalize_language_code( + source_language, default="auto" + ) + + if not normalized_target_language: + raise PdfAiError( + "Please specify a target language.", error_code="PDF_AI_INVALID_INPUT" + ) + + if ( + normalized_target_language == normalized_source_language + and normalized_source_language != "auto" + ): + raise PdfAiError( + "Please choose different source and target languages.", + error_code="PDF_AI_INVALID_INPUT", + ) text = _extract_text_from_pdf(input_path) if not text.strip(): - raise PdfAiError("Could not extract any text from the PDF.", error_code="PDF_TEXT_EMPTY") + raise PdfAiError( + "Could not extract any text from the PDF.", error_code="PDF_TEXT_EMPTY" + ) - max_chars = 10000 - truncated = text[:max_chars] - - system_prompt = ( - f"You are a professional translator. Translate the following document " - f"content into {target_language}. Preserve the original formatting and " - f"structure as much as possible. Only output the translation, nothing else." + translated = _translate_document_text( + text, + target_language=normalized_target_language, + source_language=normalized_source_language, ) - translation = _call_openrouter(system_prompt, truncated, max_tokens=2000, tool_name="pdf_translate") - page_count = text.count("[Page ") return { - "translation": translation, + "translation": translated["translation"], "pages_analyzed": page_count, - "target_language": target_language, + "target_language": normalized_target_language, + "source_language": normalized_source_language, + "detected_source_language": translated["detected_source_language"], + "provider": translated["provider"], + "chunks_translated": translated["chunks_translated"], } @@ -361,12 +749,14 @@ def extract_tables(input_path: str) -> dict: cells.append(str(val)) rows.append(cells) - result_tables.append({ - "page": page_num, - "table_index": table_index, - "headers": headers, - "rows": rows, - }) + result_tables.append( + { + "page": page_num, + "table_index": table_index, + "headers": headers, + "rows": rows, + } + ) table_index += 1 if not result_tables: @@ -385,7 +775,9 @@ def extract_tables(input_path: str) -> dict: except PdfAiError: raise except ImportError: - raise PdfAiError("tabula-py library is not installed.", error_code="TABULA_NOT_INSTALLED") + raise PdfAiError( + "tabula-py library is not installed.", error_code="TABULA_NOT_INSTALLED" + ) except Exception as e: raise PdfAiError( "Failed to extract tables.", diff --git a/backend/app/tasks/pdf_ai_tasks.py b/backend/app/tasks/pdf_ai_tasks.py index a1df423..a0686dc 100644 --- a/backend/app/tasks/pdf_ai_tasks.py +++ b/backend/app/tasks/pdf_ai_tasks.py @@ -1,4 +1,5 @@ """Celery tasks for PDF AI tools — Chat, Summarize, Translate, Table Extract.""" + import os import logging import json @@ -28,7 +29,8 @@ def _build_pdf_ai_error_payload(task_id: str, error: PdfAiError, tool: str) -> d payload = { "status": "failed", "error_code": getattr(error, "error_code", "PDF_AI_ERROR"), - "user_message": getattr(error, "user_message", str(error)) or "AI processing failed.", + "user_message": getattr(error, "user_message", str(error)) + or "AI processing failed.", "task_id": task_id, } @@ -80,9 +82,12 @@ def chat_with_pdf_task( logger.info(f"Task {task_id}: Chat with PDF completed") finalize_task_tracking( - user_id=user_id, tool="chat-pdf", - original_filename=original_filename, result=result, - usage_source=usage_source, api_key_id=api_key_id, + user_id=user_id, + tool="chat-pdf", + original_filename=original_filename, + result=result, + usage_source=usage_source, + api_key_id=api_key_id, celery_task_id=self.request.id, ) _cleanup(task_id) @@ -91,9 +96,12 @@ def chat_with_pdf_task( except PdfAiError as e: result = _build_pdf_ai_error_payload(task_id, e, "chat-pdf") finalize_task_tracking( - user_id=user_id, tool="chat-pdf", - original_filename=original_filename, result=result, - usage_source=usage_source, api_key_id=api_key_id, + user_id=user_id, + tool="chat-pdf", + original_filename=original_filename, + result=result, + usage_source=usage_source, + api_key_id=api_key_id, celery_task_id=self.request.id, ) _cleanup(task_id) @@ -103,9 +111,12 @@ def chat_with_pdf_task( logger.error(f"Task {task_id}: Unexpected error — {e}") result = {"status": "failed", "error": "An unexpected error occurred."} finalize_task_tracking( - user_id=user_id, tool="chat-pdf", - original_filename=original_filename, result=result, - usage_source=usage_source, api_key_id=api_key_id, + user_id=user_id, + tool="chat-pdf", + original_filename=original_filename, + result=result, + usage_source=usage_source, + api_key_id=api_key_id, celery_task_id=self.request.id, ) _cleanup(task_id) @@ -140,9 +151,12 @@ def summarize_pdf_task( logger.info(f"Task {task_id}: PDF summarize completed") finalize_task_tracking( - user_id=user_id, tool="summarize-pdf", - original_filename=original_filename, result=result, - usage_source=usage_source, api_key_id=api_key_id, + user_id=user_id, + tool="summarize-pdf", + original_filename=original_filename, + result=result, + usage_source=usage_source, + api_key_id=api_key_id, celery_task_id=self.request.id, ) _cleanup(task_id) @@ -151,9 +165,12 @@ def summarize_pdf_task( except PdfAiError as e: result = _build_pdf_ai_error_payload(task_id, e, "summarize-pdf") finalize_task_tracking( - user_id=user_id, tool="summarize-pdf", - original_filename=original_filename, result=result, - usage_source=usage_source, api_key_id=api_key_id, + user_id=user_id, + tool="summarize-pdf", + original_filename=original_filename, + result=result, + usage_source=usage_source, + api_key_id=api_key_id, celery_task_id=self.request.id, ) _cleanup(task_id) @@ -163,9 +180,12 @@ def summarize_pdf_task( logger.error(f"Task {task_id}: Unexpected error — {e}") result = {"status": "failed", "error": "An unexpected error occurred."} finalize_task_tracking( - user_id=user_id, tool="summarize-pdf", - original_filename=original_filename, result=result, - usage_source=usage_source, api_key_id=api_key_id, + user_id=user_id, + tool="summarize-pdf", + original_filename=original_filename, + result=result, + usage_source=usage_source, + api_key_id=api_key_id, celery_task_id=self.request.id, ) _cleanup(task_id) @@ -182,28 +202,41 @@ def translate_pdf_task( task_id: str, original_filename: str, target_language: str, + source_language: str | None = None, user_id: int | None = None, usage_source: str = "web", api_key_id: int | None = None, ): """Translate a PDF document to another language.""" try: - self.update_state(state="PROCESSING", meta={"step": "Translating document..."}) + self.update_state( + state="PROCESSING", + meta={"step": "Translating document with provider fallback..."}, + ) - data = translate_pdf(input_path, target_language) + data = translate_pdf( + input_path, target_language, source_language=source_language + ) result = { "status": "completed", "translation": data["translation"], "pages_analyzed": data["pages_analyzed"], "target_language": data["target_language"], + "source_language": data.get("source_language"), + "detected_source_language": data.get("detected_source_language"), + "provider": data.get("provider"), + "chunks_translated": data.get("chunks_translated"), } logger.info(f"Task {task_id}: PDF translate completed") finalize_task_tracking( - user_id=user_id, tool="translate-pdf", - original_filename=original_filename, result=result, - usage_source=usage_source, api_key_id=api_key_id, + user_id=user_id, + tool="translate-pdf", + original_filename=original_filename, + result=result, + usage_source=usage_source, + api_key_id=api_key_id, celery_task_id=self.request.id, ) _cleanup(task_id) @@ -212,9 +245,12 @@ def translate_pdf_task( except PdfAiError as e: result = _build_pdf_ai_error_payload(task_id, e, "translate-pdf") finalize_task_tracking( - user_id=user_id, tool="translate-pdf", - original_filename=original_filename, result=result, - usage_source=usage_source, api_key_id=api_key_id, + user_id=user_id, + tool="translate-pdf", + original_filename=original_filename, + result=result, + usage_source=usage_source, + api_key_id=api_key_id, celery_task_id=self.request.id, ) _cleanup(task_id) @@ -224,9 +260,12 @@ def translate_pdf_task( logger.error(f"Task {task_id}: Unexpected error — {e}") result = {"status": "failed", "error": "An unexpected error occurred."} finalize_task_tracking( - user_id=user_id, tool="translate-pdf", - original_filename=original_filename, result=result, - usage_source=usage_source, api_key_id=api_key_id, + user_id=user_id, + tool="translate-pdf", + original_filename=original_filename, + result=result, + usage_source=usage_source, + api_key_id=api_key_id, celery_task_id=self.request.id, ) _cleanup(task_id) @@ -260,9 +299,12 @@ def extract_tables_task( logger.info(f"Task {task_id}: Table extraction completed") finalize_task_tracking( - user_id=user_id, tool="extract-tables", - original_filename=original_filename, result=result, - usage_source=usage_source, api_key_id=api_key_id, + user_id=user_id, + tool="extract-tables", + original_filename=original_filename, + result=result, + usage_source=usage_source, + api_key_id=api_key_id, celery_task_id=self.request.id, ) _cleanup(task_id) @@ -271,9 +313,12 @@ def extract_tables_task( except PdfAiError as e: result = _build_pdf_ai_error_payload(task_id, e, "extract-tables") finalize_task_tracking( - user_id=user_id, tool="extract-tables", - original_filename=original_filename, result=result, - usage_source=usage_source, api_key_id=api_key_id, + user_id=user_id, + tool="extract-tables", + original_filename=original_filename, + result=result, + usage_source=usage_source, + api_key_id=api_key_id, celery_task_id=self.request.id, ) _cleanup(task_id) @@ -283,9 +328,12 @@ def extract_tables_task( logger.error(f"Task {task_id}: Unexpected error — {e}") result = {"status": "failed", "error": "An unexpected error occurred."} finalize_task_tracking( - user_id=user_id, tool="extract-tables", - original_filename=original_filename, result=result, - usage_source=usage_source, api_key_id=api_key_id, + user_id=user_id, + tool="extract-tables", + original_filename=original_filename, + result=result, + usage_source=usage_source, + api_key_id=api_key_id, celery_task_id=self.request.id, ) _cleanup(task_id) diff --git a/backend/config/__init__.py b/backend/config/__init__.py index 88b6dea..1df0259 100644 --- a/backend/config/__init__.py +++ b/backend/config/__init__.py @@ -26,20 +26,21 @@ def _env_or_default(name: str, default: str) -> str: class BaseConfig: """Base configuration.""" + SECRET_KEY = os.getenv("SECRET_KEY", "change-me-in-production") INTERNAL_ADMIN_SECRET = os.getenv("INTERNAL_ADMIN_SECRET", "") INTERNAL_ADMIN_EMAILS = _parse_csv_env("INTERNAL_ADMIN_EMAILS") # File upload settings - MAX_CONTENT_LENGTH = int( - os.getenv("ABSOLUTE_MAX_CONTENT_LENGTH_MB", 100) - ) * 1024 * 1024 + MAX_CONTENT_LENGTH = ( + int(os.getenv("ABSOLUTE_MAX_CONTENT_LENGTH_MB", 100)) * 1024 * 1024 + ) UPLOAD_FOLDER = _env_or_default("UPLOAD_FOLDER", "/tmp/uploads") OUTPUT_FOLDER = _env_or_default("OUTPUT_FOLDER", "/tmp/outputs") FILE_EXPIRY_SECONDS = int(os.getenv("FILE_EXPIRY_SECONDS", 1800)) - STORAGE_ALLOW_LOCAL_FALLBACK = os.getenv( - "STORAGE_ALLOW_LOCAL_FALLBACK", "true" - ).lower() == "true" + STORAGE_ALLOW_LOCAL_FALLBACK = ( + os.getenv("STORAGE_ALLOW_LOCAL_FALLBACK", "true").lower() == "true" + ) DATABASE_PATH = _env_or_default( "DATABASE_PATH", os.path.join(BASE_DIR, "data", "dociva.db") ) @@ -69,31 +70,29 @@ class BaseConfig: "application/vnd.openxmlformats-officedocument.presentationml.presentation" ], "ppt": ["application/vnd.ms-powerpoint"], - "xlsx": [ - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" - ], + "xlsx": ["application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"], "xls": ["application/vnd.ms-excel"], } # File size limits per type (bytes) FILE_SIZE_LIMITS = { - "pdf": 20 * 1024 * 1024, # 20MB - "doc": 15 * 1024 * 1024, # 15MB - "docx": 15 * 1024 * 1024, # 15MB - "html": 10 * 1024 * 1024, # 10MB - "htm": 10 * 1024 * 1024, # 10MB - "png": 10 * 1024 * 1024, # 10MB - "jpg": 10 * 1024 * 1024, # 10MB - "jpeg": 10 * 1024 * 1024, # 10MB - "webp": 10 * 1024 * 1024, # 10MB - "tiff": 15 * 1024 * 1024, # 15MB - "bmp": 15 * 1024 * 1024, # 15MB - "mp4": 50 * 1024 * 1024, # 50MB - "webm": 50 * 1024 * 1024, # 50MB - "pptx": 20 * 1024 * 1024, # 20MB - "ppt": 20 * 1024 * 1024, # 20MB - "xlsx": 15 * 1024 * 1024, # 15MB - "xls": 15 * 1024 * 1024, # 15MB + "pdf": 20 * 1024 * 1024, # 20MB + "doc": 15 * 1024 * 1024, # 15MB + "docx": 15 * 1024 * 1024, # 15MB + "html": 10 * 1024 * 1024, # 10MB + "htm": 10 * 1024 * 1024, # 10MB + "png": 10 * 1024 * 1024, # 10MB + "jpg": 10 * 1024 * 1024, # 10MB + "jpeg": 10 * 1024 * 1024, # 10MB + "webp": 10 * 1024 * 1024, # 10MB + "tiff": 15 * 1024 * 1024, # 15MB + "bmp": 15 * 1024 * 1024, # 15MB + "mp4": 50 * 1024 * 1024, # 50MB + "webm": 50 * 1024 * 1024, # 50MB + "pptx": 20 * 1024 * 1024, # 20MB + "ppt": 20 * 1024 * 1024, # 20MB + "xlsx": 15 * 1024 * 1024, # 15MB + "xls": 15 * 1024 * 1024, # 15MB } # Redis @@ -109,7 +108,7 @@ class BaseConfig: AWS_S3_BUCKET = os.getenv("AWS_S3_BUCKET", "dociva-temp-files") AWS_S3_REGION = os.getenv("AWS_S3_REGION", "eu-west-1") - # CORS + # CORS CORS_ORIGINS = os.getenv("CORS_ORIGINS", "http://localhost:5173").split(",") # Rate Limiting @@ -118,11 +117,20 @@ class BaseConfig: # OpenRouter AI OPENROUTER_API_KEY = os.getenv("OPENROUTER_API_KEY", "") - OPENROUTER_MODEL = os.getenv("OPENROUTER_MODEL", "nvidia/nemotron-3-super-120b-a12b:free") + OPENROUTER_MODEL = os.getenv( + "OPENROUTER_MODEL", "nvidia/nemotron-3-super-120b-a12b:free" + ) OPENROUTER_BASE_URL = os.getenv( "OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1/chat/completions" ) + # Premium translation provider (recommended for Translate PDF) + DEEPL_API_KEY = os.getenv("DEEPL_API_KEY", "") + DEEPL_API_URL = os.getenv( + "DEEPL_API_URL", "https://api-free.deepl.com/v2/translate" + ) + DEEPL_TIMEOUT_SECONDS = int(os.getenv("DEEPL_TIMEOUT_SECONDS", 90)) + # SMTP (for password reset emails) SMTP_HOST = os.getenv("SMTP_HOST", "") SMTP_PORT = int(os.getenv("SMTP_PORT", 587)) @@ -156,12 +164,14 @@ class BaseConfig: class DevelopmentConfig(BaseConfig): """Development configuration.""" + DEBUG = True TESTING = False class ProductionConfig(BaseConfig): """Production configuration.""" + DEBUG = False TESTING = False SESSION_COOKIE_SECURE = True @@ -172,6 +182,7 @@ class ProductionConfig(BaseConfig): class TestingConfig(BaseConfig): """Testing configuration.""" + DEBUG = True TESTING = True UPLOAD_FOLDER = "/tmp/test_uploads" diff --git a/backend/tests/test_pdf_translate_service.py b/backend/tests/test_pdf_translate_service.py new file mode 100644 index 0000000..67774ea --- /dev/null +++ b/backend/tests/test_pdf_translate_service.py @@ -0,0 +1,93 @@ +"""Tests for the resilient PDF translation workflow.""" + +from app.services.pdf_ai_service import DeepLSettings, PdfAiError, translate_pdf + + +def test_translate_pdf_prefers_premium_provider(monkeypatch): + """Should use the premium provider when configured and available.""" + monkeypatch.setattr( + "app.services.pdf_ai_service._extract_text_from_pdf", + lambda _path: "[Page 1]\nHello world\n\n[Page 2]\nSecond page", + ) + monkeypatch.setattr( + "app.services.pdf_ai_service._get_deepl_settings", + lambda: DeepLSettings( + api_key="key", + base_url="https://api-free.deepl.com/v2/translate", + timeout_seconds=90, + ), + ) + monkeypatch.setattr( + "app.services.pdf_ai_service._translate_with_retry", + lambda action, provider_name: action(), + ) + monkeypatch.setattr( + "app.services.pdf_ai_service._call_deepl_translate", + lambda chunk, target_language, source_language=None: { + "translation": f"translated::{chunk}", + "provider": "deepl", + "detected_source_language": "en", + }, + ) + + result = translate_pdf("/tmp/demo.pdf", "fr", source_language="en") + + assert result["provider"] == "deepl" + assert result["target_language"] == "fr" + assert result["detected_source_language"] == "en" + assert "translated::" in result["translation"] + + +def test_translate_pdf_falls_back_when_premium_provider_fails(monkeypatch): + """Should fall back to OpenRouter if the premium provider fails.""" + monkeypatch.setattr( + "app.services.pdf_ai_service._extract_text_from_pdf", + lambda _path: "[Page 1]\nHello world", + ) + monkeypatch.setattr( + "app.services.pdf_ai_service._get_deepl_settings", + lambda: DeepLSettings( + api_key="key", + base_url="https://api-free.deepl.com/v2/translate", + timeout_seconds=90, + ), + ) + monkeypatch.setattr( + "app.services.pdf_ai_service._translate_with_retry", + lambda action, provider_name: action(), + ) + + def fail_deepl(*_args, **_kwargs): + raise PdfAiError("DeepL unavailable", error_code="DEEPL_SERVER_ERROR") + + monkeypatch.setattr("app.services.pdf_ai_service._call_deepl_translate", fail_deepl) + monkeypatch.setattr( + "app.services.pdf_ai_service._call_openrouter_translate", + lambda chunk, target_language, source_language=None: { + "translation": f"fallback::{chunk}", + "provider": "openrouter", + "detected_source_language": "en", + }, + ) + + result = translate_pdf("/tmp/demo.pdf", "de", source_language="auto") + + assert result["provider"] == "openrouter" + assert result["detected_source_language"] == "en" + assert result["translation"].startswith("fallback::") + + +def test_translate_pdf_rejects_identical_languages(monkeypatch): + """Should reject no-op translation requests.""" + monkeypatch.setattr( + "app.services.pdf_ai_service._extract_text_from_pdf", + lambda _path: "[Page 1]\nHello world", + ) + + try: + translate_pdf("/tmp/demo.pdf", "fr", source_language="fr") + except PdfAiError as error: + assert error.error_code == "PDF_AI_INVALID_INPUT" + assert "different source and target languages" in error.user_message + else: + raise AssertionError("Expected identical language validation to fail") diff --git a/frontend/public/sitemap.xml b/frontend/public/sitemap.xml index 4f794d1..d6d973a 100644 --- a/frontend/public/sitemap.xml +++ b/frontend/public/sitemap.xml @@ -1,1473 +1,19 @@ - - - https://dociva.io/ - 2026-03-27 - daily - 1.0 - - - https://dociva.io/about - 2026-03-27 - monthly - 0.4 - - - https://dociva.io/contact - 2026-03-27 - monthly - 0.4 - - - https://dociva.io/privacy - 2026-03-27 - yearly - 0.3 - - - https://dociva.io/terms - 2026-03-27 - yearly - 0.3 - - - https://dociva.io/pricing - 2026-03-27 - monthly - 0.7 - - - https://dociva.io/blog - 2026-03-27 - weekly - 0.6 - - - https://dociva.io/developers - 2026-03-27 - monthly - 0.5 - - - https://dociva.io/blog/how-to-compress-pdf-online - 2026-03-27 - monthly - 0.6 - - - https://dociva.io/blog/convert-images-without-losing-quality - 2026-03-27 - monthly - 0.6 - - - https://dociva.io/blog/ocr-extract-text-from-images - 2026-03-27 - monthly - 0.6 - - - https://dociva.io/blog/merge-split-pdf-files - 2026-03-27 - monthly - 0.6 - - - https://dociva.io/blog/ai-chat-with-pdf-documents - 2026-03-27 - monthly - 0.6 - - - https://dociva.io/tools/pdf-to-word - 2026-03-27 - weekly - 0.9 - - - https://dociva.io/tools/word-to-pdf - 2026-03-27 - weekly - 0.9 - - - https://dociva.io/tools/compress-pdf - 2026-03-27 - weekly - 0.9 - - - https://dociva.io/tools/merge-pdf - 2026-03-27 - weekly - 0.9 - - - https://dociva.io/tools/split-pdf - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/tools/rotate-pdf - 2026-03-27 - weekly - 0.7 - - - https://dociva.io/tools/pdf-to-images - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/tools/images-to-pdf - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/tools/watermark-pdf - 2026-03-27 - weekly - 0.7 - - - https://dociva.io/tools/protect-pdf - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/tools/unlock-pdf - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/tools/page-numbers - 2026-03-27 - weekly - 0.7 - - - https://dociva.io/tools/pdf-editor - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/tools/pdf-flowchart - 2026-03-27 - weekly - 0.7 - - - https://dociva.io/tools/pdf-to-excel - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/tools/remove-watermark-pdf - 2026-03-27 - weekly - 0.7 - - - https://dociva.io/tools/reorder-pdf - 2026-03-27 - weekly - 0.7 - - - https://dociva.io/tools/extract-pages - 2026-03-27 - weekly - 0.7 - - - https://dociva.io/tools/image-converter - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/tools/image-resize - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/tools/compress-image - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/tools/ocr - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/tools/remove-background - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/tools/image-to-svg - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/tools/html-to-pdf - 2026-03-27 - weekly - 0.7 - - - https://dociva.io/tools/chat-pdf - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/tools/summarize-pdf - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/tools/translate-pdf - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/tools/extract-tables - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/tools/qr-code - 2026-03-27 - weekly - 0.7 - - - https://dociva.io/tools/video-to-gif - 2026-03-27 - weekly - 0.7 - - - https://dociva.io/tools/word-counter - 2026-03-27 - weekly - 0.6 - - - https://dociva.io/tools/text-cleaner - 2026-03-27 - weekly - 0.6 - - - https://dociva.io/tools/pdf-to-pptx - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/tools/excel-to-pdf - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/tools/pptx-to-pdf - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/tools/sign-pdf - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/tools/crop-pdf - 2026-03-27 - weekly - 0.7 - - - https://dociva.io/tools/flatten-pdf - 2026-03-27 - weekly - 0.7 - - - https://dociva.io/tools/repair-pdf - 2026-03-27 - weekly - 0.7 - - - https://dociva.io/tools/pdf-metadata - 2026-03-27 - weekly - 0.6 - - - https://dociva.io/tools/image-crop - 2026-03-27 - weekly - 0.7 - - - https://dociva.io/tools/image-rotate-flip - 2026-03-27 - weekly - 0.7 - - - https://dociva.io/tools/barcode-generator - 2026-03-27 - weekly - 0.7 - - - https://dociva.io/pdf-to-word - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/pdf-to-word - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/word-to-pdf - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/word-to-pdf - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/compress-pdf-online - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/compress-pdf-online - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/convert-jpg-to-pdf - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/convert-jpg-to-pdf - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/merge-pdf-files - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/merge-pdf-files - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/remove-pdf-password - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/remove-pdf-password - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/pdf-to-word-editable - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/pdf-to-word-editable - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/convert-pdf-to-text - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/convert-pdf-to-text - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/split-pdf-online - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/split-pdf-online - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/jpg-to-pdf - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/jpg-to-pdf - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/png-to-pdf - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/png-to-pdf - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/images-to-pdf-online - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/images-to-pdf-online - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/pdf-to-jpg - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/pdf-to-jpg - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/pdf-to-png - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/pdf-to-png - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/compress-pdf-for-email - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/compress-pdf-for-email - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/compress-scanned-pdf - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/compress-scanned-pdf - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/merge-pdf-online-free - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/merge-pdf-online-free - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/combine-pdf-files - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/combine-pdf-files - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/extract-pages-from-pdf - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/extract-pages-from-pdf - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/reorder-pdf-pages - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/reorder-pdf-pages - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/rotate-pdf-pages - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/rotate-pdf-pages - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/add-page-numbers-to-pdf - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/add-page-numbers-to-pdf - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/protect-pdf-with-password - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/protect-pdf-with-password - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/unlock-pdf-online - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/unlock-pdf-online - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/watermark-pdf-online - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/watermark-pdf-online - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/remove-watermark-from-pdf - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/remove-watermark-from-pdf - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/edit-pdf-online-free - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/edit-pdf-online-free - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/pdf-to-excel-online - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/pdf-to-excel-online - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/extract-tables-from-pdf - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/extract-tables-from-pdf - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/html-to-pdf-online - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/html-to-pdf-online - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/scan-pdf-to-text - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/scan-pdf-to-text - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/chat-with-pdf - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/chat-with-pdf - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/summarize-pdf-online - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/summarize-pdf-online - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/translate-pdf-online - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/translate-pdf-online - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/convert-image-to-pdf - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/convert-image-to-pdf - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/convert-webp-to-jpg - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/convert-webp-to-jpg - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/resize-image-online - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/resize-image-online - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/compress-image-online - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/compress-image-online - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/remove-image-background - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/remove-image-background - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/pdf-to-word-editable-free - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/pdf-to-word-editable-free - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/compress-pdf-to-100kb - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/compress-pdf-to-100kb - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/ai-extract-text-from-pdf - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/ai-extract-text-from-pdf - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/pdf-to-excel-accurate-free - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/pdf-to-excel-accurate-free - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/split-pdf-online-free - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/split-pdf-online-free - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/compress-pdf-online-free - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/compress-pdf-online-free - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/unlock-pdf-online-free - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/unlock-pdf-online-free - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/summarize-pdf-ai - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/summarize-pdf-ai - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/convert-pdf-to-text-ai - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/convert-pdf-to-text-ai - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/pdf-to-jpg-high-quality - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/pdf-to-jpg-high-quality - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/jpg-to-pdf-online-free - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/jpg-to-pdf-online-free - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/reduce-pdf-size-for-email - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/reduce-pdf-size-for-email - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/ocr-for-scanned-pdfs - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/ocr-for-scanned-pdfs - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/remove-watermark-from-pdf-online - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/remove-watermark-from-pdf-online - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/add-watermark-to-pdf-online - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/add-watermark-to-pdf-online - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/repair-corrupted-pdf-online - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/repair-corrupted-pdf-online - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/rotate-pdf-pages-online - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/rotate-pdf-pages-online - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/reorder-pdf-pages-online - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/reorder-pdf-pages-online - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/pdf-to-png-online - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/pdf-to-png-online - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/images-to-pdf-multiple - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/images-to-pdf-multiple - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/split-pdf-by-range-online - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/split-pdf-by-range-online - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/compress-scanned-pdf-online - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/compress-scanned-pdf-online - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/pdf-metadata-editor-online - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/pdf-metadata-editor-online - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/add-page-numbers-to-pdf-online - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/add-page-numbers-to-pdf-online - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/protect-pdf-with-password-online - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/protect-pdf-with-password-online - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/unlock-encrypted-pdf-online - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/unlock-encrypted-pdf-online - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/ocr-table-extraction-from-pdf - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/ocr-table-extraction-from-pdf - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/pdf-to-excel-converter-online - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/pdf-to-excel-converter-online - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/extract-text-from-protected-pdf - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/extract-text-from-protected-pdf - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/bulk-convert-pdf-to-word - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/bulk-convert-pdf-to-word - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/compress-pdf-for-web-upload - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/compress-pdf-for-web-upload - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/ocr-multi-language-pdf - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/ocr-multi-language-pdf - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/summarize-long-pdf-ai - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/summarize-long-pdf-ai - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/convert-pdf-to-ppt-online - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/convert-pdf-to-ppt-online - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/pdf-to-pptx-free-online - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/pdf-to-pptx-free-online - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/دمج-ملفات-pdf-مجاناً - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/دمج-ملفات-pdf-مجاناً - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/ضغط-بي-دي-اف-اونلاين - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/ضغط-بي-دي-اف-اونلاين - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/تحويل-pdf-الى-word-قابل-للتعديل - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/تحويل-pdf-الى-word-قابل-للتعديل - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/تحويل-jpg-الى-pdf-اونلاين - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/تحويل-jpg-الى-pdf-اونلاين - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/فصل-صفحات-pdf-اونلاين - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/فصل-صفحات-pdf-اونلاين - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/ازالة-كلمة-مرور-من-pdf - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/ازالة-كلمة-مرور-من-pdf - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/تحويل-pdf-الى-نص-باستخدام-ocr - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/تحويل-pdf-الى-نص-باستخدام-ocr - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/تحويل-pdf-الى-excel-اونلاين - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/تحويل-pdf-الى-excel-اونلاين - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/تحويل-pdf-الى-صور - 2026-03-27 - weekly - 0.88 - - - https://dociva.io/ar/تحويل-pdf-الى-صور - 2026-03-27 - weekly - 0.8 - - - https://dociva.io/best-pdf-tools - 2026-03-27 - weekly - 0.82 - - - https://dociva.io/ar/best-pdf-tools - 2026-03-27 - weekly - 0.74 - - - https://dociva.io/free-pdf-tools-online - 2026-03-27 - weekly - 0.82 - - - https://dociva.io/ar/free-pdf-tools-online - 2026-03-27 - weekly - 0.74 - - - https://dociva.io/convert-files-online - 2026-03-27 - weekly - 0.82 - - - https://dociva.io/ar/convert-files-online - 2026-03-27 - weekly - 0.74 - - - https://dociva.io/pdf-converter-tools - 2026-03-27 - weekly - 0.82 - - - https://dociva.io/ar/pdf-converter-tools - 2026-03-27 - weekly - 0.74 - - - https://dociva.io/secure-pdf-tools - 2026-03-27 - weekly - 0.82 - - - https://dociva.io/ar/secure-pdf-tools - 2026-03-27 - weekly - 0.74 - - - https://dociva.io/ai-document-tools - 2026-03-27 - weekly - 0.82 - - - https://dociva.io/ar/ai-document-tools - 2026-03-27 - weekly - 0.74 - - - https://dociva.io/image-to-pdf-tools - 2026-03-27 - weekly - 0.82 - - - https://dociva.io/ar/image-to-pdf-tools - 2026-03-27 - weekly - 0.74 - - - https://dociva.io/online-image-tools - 2026-03-27 - weekly - 0.82 - - - https://dociva.io/ar/online-image-tools - 2026-03-27 - weekly - 0.74 - - - https://dociva.io/office-to-pdf-tools - 2026-03-27 - weekly - 0.82 - - - https://dociva.io/ar/office-to-pdf-tools - 2026-03-27 - weekly - 0.74 - - - https://dociva.io/scanned-document-tools - 2026-03-27 - weekly - 0.82 - - - https://dociva.io/ar/scanned-document-tools - 2026-03-27 - weekly - 0.74 - - - https://dociva.io/arabic-pdf-tools - 2026-03-27 - weekly - 0.82 - - - https://dociva.io/ar/arabic-pdf-tools - 2026-03-27 - weekly - 0.74 - - + + + https://dociva.io/sitemaps/static.xml + 2026-03-30 + + + https://dociva.io/sitemaps/blog.xml + 2026-03-30 + + + https://dociva.io/sitemaps/tools.xml + 2026-03-30 + + + https://dociva.io/sitemaps/seo.xml + 2026-03-30 + + diff --git a/frontend/public/sitemaps/blog.xml b/frontend/public/sitemaps/blog.xml index e80f10a..b6a1b55 100644 --- a/frontend/public/sitemaps/blog.xml +++ b/frontend/public/sitemaps/blog.xml @@ -2,31 +2,31 @@ https://dociva.io/blog/how-to-compress-pdf-online - 2026-03-29 + 2026-03-30 monthly 0.6 https://dociva.io/blog/convert-images-without-losing-quality - 2026-03-29 + 2026-03-30 monthly 0.6 https://dociva.io/blog/ocr-extract-text-from-images - 2026-03-29 + 2026-03-30 monthly 0.6 https://dociva.io/blog/merge-split-pdf-files - 2026-03-29 + 2026-03-30 monthly 0.6 https://dociva.io/blog/ai-chat-with-pdf-documents - 2026-03-29 + 2026-03-30 monthly 0.6 diff --git a/frontend/public/sitemaps/seo.xml b/frontend/public/sitemaps/seo.xml index 87ce702..86e4802 100644 --- a/frontend/public/sitemaps/seo.xml +++ b/frontend/public/sitemaps/seo.xml @@ -2,1129 +2,1129 @@ https://dociva.io/pdf-to-word - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/pdf-to-word - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/word-to-pdf - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/word-to-pdf - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/compress-pdf-online - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/compress-pdf-online - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/convert-jpg-to-pdf - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/convert-jpg-to-pdf - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/merge-pdf-files - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/merge-pdf-files - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/remove-pdf-password - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/remove-pdf-password - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/pdf-to-word-editable - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/pdf-to-word-editable - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/convert-pdf-to-text - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/convert-pdf-to-text - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/split-pdf-online - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/split-pdf-online - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/jpg-to-pdf - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/jpg-to-pdf - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/png-to-pdf - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/png-to-pdf - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/images-to-pdf-online - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/images-to-pdf-online - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/pdf-to-jpg - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/pdf-to-jpg - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/pdf-to-png - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/pdf-to-png - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/compress-pdf-for-email - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/compress-pdf-for-email - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/compress-scanned-pdf - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/compress-scanned-pdf - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/merge-pdf-online-free - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/merge-pdf-online-free - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/combine-pdf-files - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/combine-pdf-files - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/extract-pages-from-pdf - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/extract-pages-from-pdf - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/reorder-pdf-pages - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/reorder-pdf-pages - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/rotate-pdf-pages - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/rotate-pdf-pages - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/add-page-numbers-to-pdf - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/add-page-numbers-to-pdf - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/protect-pdf-with-password - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/protect-pdf-with-password - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/unlock-pdf-online - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/unlock-pdf-online - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/watermark-pdf-online - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/watermark-pdf-online - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/remove-watermark-from-pdf - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/remove-watermark-from-pdf - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/edit-pdf-online-free - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/edit-pdf-online-free - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/pdf-to-excel-online - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/pdf-to-excel-online - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/extract-tables-from-pdf - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/extract-tables-from-pdf - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/html-to-pdf-online - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/html-to-pdf-online - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/scan-pdf-to-text - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/scan-pdf-to-text - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/chat-with-pdf - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/chat-with-pdf - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/summarize-pdf-online - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/summarize-pdf-online - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/translate-pdf-online - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/translate-pdf-online - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/convert-image-to-pdf - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/convert-image-to-pdf - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/convert-webp-to-jpg - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/convert-webp-to-jpg - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/resize-image-online - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/resize-image-online - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/compress-image-online - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/compress-image-online - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/remove-image-background - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/remove-image-background - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/pdf-to-word-editable-free - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/pdf-to-word-editable-free - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/compress-pdf-to-100kb - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/compress-pdf-to-100kb - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/ai-extract-text-from-pdf - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/ai-extract-text-from-pdf - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/pdf-to-excel-accurate-free - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/pdf-to-excel-accurate-free - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/split-pdf-online-free - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/split-pdf-online-free - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/compress-pdf-online-free - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/compress-pdf-online-free - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/unlock-pdf-online-free - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/unlock-pdf-online-free - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/summarize-pdf-ai - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/summarize-pdf-ai - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/convert-pdf-to-text-ai - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/convert-pdf-to-text-ai - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/pdf-to-jpg-high-quality - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/pdf-to-jpg-high-quality - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/jpg-to-pdf-online-free - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/jpg-to-pdf-online-free - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/reduce-pdf-size-for-email - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/reduce-pdf-size-for-email - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/ocr-for-scanned-pdfs - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/ocr-for-scanned-pdfs - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/remove-watermark-from-pdf-online - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/remove-watermark-from-pdf-online - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/add-watermark-to-pdf-online - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/add-watermark-to-pdf-online - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/repair-corrupted-pdf-online - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/repair-corrupted-pdf-online - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/rotate-pdf-pages-online - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/rotate-pdf-pages-online - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/reorder-pdf-pages-online - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/reorder-pdf-pages-online - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/pdf-to-png-online - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/pdf-to-png-online - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/images-to-pdf-multiple - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/images-to-pdf-multiple - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/split-pdf-by-range-online - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/split-pdf-by-range-online - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/compress-scanned-pdf-online - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/compress-scanned-pdf-online - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/pdf-metadata-editor-online - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/pdf-metadata-editor-online - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/add-page-numbers-to-pdf-online - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/add-page-numbers-to-pdf-online - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/protect-pdf-with-password-online - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/protect-pdf-with-password-online - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/unlock-encrypted-pdf-online - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/unlock-encrypted-pdf-online - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/ocr-table-extraction-from-pdf - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/ocr-table-extraction-from-pdf - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/pdf-to-excel-converter-online - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/pdf-to-excel-converter-online - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/extract-text-from-protected-pdf - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/extract-text-from-protected-pdf - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/bulk-convert-pdf-to-word - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/bulk-convert-pdf-to-word - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/compress-pdf-for-web-upload - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/compress-pdf-for-web-upload - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/ocr-multi-language-pdf - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/ocr-multi-language-pdf - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/summarize-long-pdf-ai - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/summarize-long-pdf-ai - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/convert-pdf-to-ppt-online - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/convert-pdf-to-ppt-online - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/pdf-to-pptx-free-online - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/pdf-to-pptx-free-online - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/دمج-ملفات-pdf-مجاناً - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/دمج-ملفات-pdf-مجاناً - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/ضغط-بي-دي-اف-اونلاين - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/ضغط-بي-دي-اف-اونلاين - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/تحويل-pdf-الى-word-قابل-للتعديل - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/تحويل-pdf-الى-word-قابل-للتعديل - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/تحويل-jpg-الى-pdf-اونلاين - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/تحويل-jpg-الى-pdf-اونلاين - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/فصل-صفحات-pdf-اونلاين - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/فصل-صفحات-pdf-اونلاين - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/ازالة-كلمة-مرور-من-pdf - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/ازالة-كلمة-مرور-من-pdf - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/تحويل-pdf-الى-نص-باستخدام-ocr - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/تحويل-pdf-الى-نص-باستخدام-ocr - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/تحويل-pdf-الى-excel-اونلاين - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/تحويل-pdf-الى-excel-اونلاين - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/تحويل-pdf-الى-صور - 2026-03-29 + 2026-03-30 weekly 0.88 https://dociva.io/ar/تحويل-pdf-الى-صور - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/best-pdf-tools - 2026-03-29 + 2026-03-30 weekly 0.82 https://dociva.io/ar/best-pdf-tools - 2026-03-29 + 2026-03-30 weekly 0.74 https://dociva.io/free-pdf-tools-online - 2026-03-29 + 2026-03-30 weekly 0.82 https://dociva.io/ar/free-pdf-tools-online - 2026-03-29 + 2026-03-30 weekly 0.74 https://dociva.io/convert-files-online - 2026-03-29 + 2026-03-30 weekly 0.82 https://dociva.io/ar/convert-files-online - 2026-03-29 + 2026-03-30 weekly 0.74 https://dociva.io/pdf-converter-tools - 2026-03-29 + 2026-03-30 weekly 0.82 https://dociva.io/ar/pdf-converter-tools - 2026-03-29 + 2026-03-30 weekly 0.74 https://dociva.io/secure-pdf-tools - 2026-03-29 + 2026-03-30 weekly 0.82 https://dociva.io/ar/secure-pdf-tools - 2026-03-29 + 2026-03-30 weekly 0.74 https://dociva.io/ai-document-tools - 2026-03-29 + 2026-03-30 weekly 0.82 https://dociva.io/ar/ai-document-tools - 2026-03-29 + 2026-03-30 weekly 0.74 https://dociva.io/image-to-pdf-tools - 2026-03-29 + 2026-03-30 weekly 0.82 https://dociva.io/ar/image-to-pdf-tools - 2026-03-29 + 2026-03-30 weekly 0.74 https://dociva.io/online-image-tools - 2026-03-29 + 2026-03-30 weekly 0.82 https://dociva.io/ar/online-image-tools - 2026-03-29 + 2026-03-30 weekly 0.74 https://dociva.io/office-to-pdf-tools - 2026-03-29 + 2026-03-30 weekly 0.82 https://dociva.io/ar/office-to-pdf-tools - 2026-03-29 + 2026-03-30 weekly 0.74 https://dociva.io/scanned-document-tools - 2026-03-29 + 2026-03-30 weekly 0.82 https://dociva.io/ar/scanned-document-tools - 2026-03-29 + 2026-03-30 weekly 0.74 https://dociva.io/arabic-pdf-tools - 2026-03-29 + 2026-03-30 weekly 0.82 https://dociva.io/ar/arabic-pdf-tools - 2026-03-29 + 2026-03-30 weekly 0.74 diff --git a/frontend/public/sitemaps/static.xml b/frontend/public/sitemaps/static.xml index b8e0d48..5c0a9a0 100644 --- a/frontend/public/sitemaps/static.xml +++ b/frontend/public/sitemaps/static.xml @@ -2,55 +2,55 @@ https://dociva.io/ - 2026-03-29 + 2026-03-30 daily 1.0 https://dociva.io/tools - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/about - 2026-03-29 + 2026-03-30 monthly 0.4 https://dociva.io/contact - 2026-03-29 + 2026-03-30 monthly 0.4 https://dociva.io/privacy - 2026-03-29 + 2026-03-30 yearly 0.3 https://dociva.io/terms - 2026-03-29 + 2026-03-30 yearly 0.3 https://dociva.io/pricing - 2026-03-29 + 2026-03-30 monthly 0.7 https://dociva.io/blog - 2026-03-29 + 2026-03-30 weekly 0.6 https://dociva.io/developers - 2026-03-29 + 2026-03-30 monthly 0.5 diff --git a/frontend/public/sitemaps/tools.xml b/frontend/public/sitemaps/tools.xml index b0c77e0..b27f322 100644 --- a/frontend/public/sitemaps/tools.xml +++ b/frontend/public/sitemaps/tools.xml @@ -2,265 +2,265 @@ https://dociva.io/tools/pdf-to-word - 2026-03-29 + 2026-03-30 weekly 0.9 https://dociva.io/tools/word-to-pdf - 2026-03-29 + 2026-03-30 weekly 0.9 https://dociva.io/tools/compress-pdf - 2026-03-29 + 2026-03-30 weekly 0.9 https://dociva.io/tools/merge-pdf - 2026-03-29 + 2026-03-30 weekly 0.9 https://dociva.io/tools/split-pdf - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/tools/rotate-pdf - 2026-03-29 + 2026-03-30 weekly 0.7 https://dociva.io/tools/pdf-to-images - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/tools/images-to-pdf - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/tools/watermark-pdf - 2026-03-29 + 2026-03-30 weekly 0.7 https://dociva.io/tools/protect-pdf - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/tools/unlock-pdf - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/tools/page-numbers - 2026-03-29 + 2026-03-30 weekly 0.7 https://dociva.io/tools/pdf-editor - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/tools/pdf-flowchart - 2026-03-29 + 2026-03-30 weekly 0.7 https://dociva.io/tools/pdf-to-excel - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/tools/remove-watermark-pdf - 2026-03-29 + 2026-03-30 weekly 0.7 https://dociva.io/tools/reorder-pdf - 2026-03-29 + 2026-03-30 weekly 0.7 https://dociva.io/tools/extract-pages - 2026-03-29 + 2026-03-30 weekly 0.7 https://dociva.io/tools/image-converter - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/tools/image-resize - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/tools/compress-image - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/tools/ocr - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/tools/remove-background - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/tools/image-to-svg - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/tools/html-to-pdf - 2026-03-29 + 2026-03-30 weekly 0.7 https://dociva.io/tools/chat-pdf - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/tools/summarize-pdf - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/tools/translate-pdf - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/tools/extract-tables - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/tools/qr-code - 2026-03-29 + 2026-03-30 weekly 0.7 https://dociva.io/tools/video-to-gif - 2026-03-29 + 2026-03-30 weekly 0.7 https://dociva.io/tools/word-counter - 2026-03-29 + 2026-03-30 weekly 0.6 https://dociva.io/tools/text-cleaner - 2026-03-29 + 2026-03-30 weekly 0.6 https://dociva.io/tools/pdf-to-pptx - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/tools/excel-to-pdf - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/tools/pptx-to-pdf - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/tools/sign-pdf - 2026-03-29 + 2026-03-30 weekly 0.8 https://dociva.io/tools/crop-pdf - 2026-03-29 + 2026-03-30 weekly 0.7 https://dociva.io/tools/flatten-pdf - 2026-03-29 + 2026-03-30 weekly 0.7 https://dociva.io/tools/repair-pdf - 2026-03-29 + 2026-03-30 weekly 0.7 https://dociva.io/tools/pdf-metadata - 2026-03-29 + 2026-03-30 weekly 0.6 https://dociva.io/tools/image-crop - 2026-03-29 + 2026-03-30 weekly 0.7 https://dociva.io/tools/image-rotate-flip - 2026-03-29 + 2026-03-30 weekly 0.7 https://dociva.io/tools/barcode-generator - 2026-03-29 + 2026-03-30 weekly 0.7 diff --git a/frontend/src/components/tools/TranslatePdf.tsx b/frontend/src/components/tools/TranslatePdf.tsx index 5593b59..770495a 100644 --- a/frontend/src/components/tools/TranslatePdf.tsx +++ b/frontend/src/components/tools/TranslatePdf.tsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { Helmet } from 'react-helmet-async'; -import { Languages } from 'lucide-react'; +import { Languages, ShieldCheck, Sparkles } from 'lucide-react'; import FileUploader from '@/components/shared/FileUploader'; import ProgressBar from '@/components/shared/ProgressBar'; import AdSlot from '@/components/layout/AdSlot'; @@ -26,11 +26,22 @@ const LANGUAGES = [ { value: 'it', label: 'Italiano' }, ]; +const getLanguageLabel = (value: string) => { + if (!value || value === 'auto') { + return null; + } + + return LANGUAGES.find((language) => language.value === value)?.label ?? value; +}; + export default function TranslatePdf() { const { t } = useTranslation(); const [phase, setPhase] = useState<'upload' | 'processing' | 'done'>('upload'); + const [sourceLang, setSourceLang] = useState('auto'); const [targetLang, setTargetLang] = useState('en'); const [translation, setTranslation] = useState(''); + const [provider, setProvider] = useState(''); + const [detectedSourceLanguage, setDetectedSourceLanguage] = useState(''); const { file, uploadProgress, isUploading, taskId, @@ -39,7 +50,7 @@ export default function TranslatePdf() { endpoint: '/pdf-ai/translate', maxSizeMB: 20, acceptedTypes: ['pdf'], - extraData: { target_language: targetLang }, + extraData: { target_language: targetLang, source_language: sourceLang }, }); const { status, result, error: taskError } = useTaskPolling({ @@ -47,6 +58,8 @@ export default function TranslatePdf() { onComplete: (r) => { setPhase('done'); setTranslation(r.translation || ''); + setProvider(r.provider || ''); + setDetectedSourceLanguage(r.detected_source_language || ''); dispatchRatingPrompt('translate-pdf'); }, onError: () => setPhase('done'), @@ -63,7 +76,17 @@ export default function TranslatePdf() { if (id) setPhase('processing'); }; - const handleReset = () => { reset(); setPhase('upload'); setTargetLang('en'); setTranslation(''); }; + const handleReset = () => { + reset(); + setPhase('upload'); + setSourceLang('auto'); + setTargetLang('en'); + setTranslation(''); + setProvider(''); + setDetectedSourceLanguage(''); + }; + + const resolvedDetectedLanguage = getLanguageLabel(detectedSourceLanguage) || getLanguageLabel(sourceLang); const schema = generateToolSchema({ name: t('tools.translatePdf.title'), @@ -103,15 +126,44 @@ export default function TranslatePdf() { {file && !isUploading && ( <>
- - +
+ +
+

+ {t('tools.translatePdf.engineTitle')} +

+

+ {t('tools.translatePdf.engineDescription')} +

+
+
+ +
+
+ + +
+ +
+ + +
+