Merge pull request #3 from scub-france/docker
push ci and docker in project
This commit is contained in:
commit
0d833771ff
6 changed files with 125 additions and 78 deletions
44
.github/workflows/release.yml
vendored
Normal file
44
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
name: Build and Push Docker Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- docker
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-and-push:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Log in to GitHub Container Registry
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.actor }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Set image name
|
||||||
|
id: image_name
|
||||||
|
run: |
|
||||||
|
IMAGE_NAME=ghcr.io/${{ github.repository }}:${{ github.sha }}
|
||||||
|
echo "IMAGE_NAME=${IMAGE_NAME,,}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
run: |
|
||||||
|
docker build -t ${{ steps.image_name.outputs.IMAGE_NAME }} .
|
||||||
|
|
||||||
|
- name: Push Docker image
|
||||||
|
run: |
|
||||||
|
docker push ${{ steps.image_name.outputs.IMAGE_NAME }}
|
||||||
|
- name: Tag image as latest
|
||||||
|
run: |
|
||||||
|
docker tag ${{ steps.image_name.outputs.IMAGE_NAME }} ghcr.io/${{ github.repository }}:latest
|
||||||
|
docker push ghcr.io/${{ github.repository }}:latest
|
||||||
7
Dockerfile
Normal file
7
Dockerfile
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
FROM python:3.12
|
||||||
|
WORKDIR /app
|
||||||
|
COPY backend/requirements.txt ./backend/requirements.txt
|
||||||
|
RUN pip install --no-cache-dir -r backend/requirements.txt
|
||||||
|
COPY . .
|
||||||
|
RUN chmod +x run_app.sh
|
||||||
|
ENTRYPOINT ["./run_app.sh"]
|
||||||
|
|
@ -36,7 +36,7 @@ CLEANUP_AGE_HOURS = 24
|
||||||
CLEANUP_INTERVAL = 3600 # 1 hour
|
CLEANUP_INTERVAL = 3600 # 1 hour
|
||||||
|
|
||||||
# Model settings
|
# Model settings
|
||||||
MODEL_PATH = "ds4sd/SmolDocling-256M-preview-mlx-bf16"
|
MODEL_PATH = "ds4sd/SmolDocling-256M-preview"
|
||||||
MAX_TOKENS = 4096
|
MAX_TOKENS = 4096
|
||||||
|
|
||||||
# Zone colors for visualization
|
# Zone colors for visualization
|
||||||
|
|
|
||||||
|
|
@ -2,36 +2,37 @@
|
||||||
# /// script
|
# /// script
|
||||||
# requires-python = ">=3.12"
|
# requires-python = ">=3.12"
|
||||||
# dependencies = [
|
# dependencies = [
|
||||||
# "docling-core",
|
# "transformers>=4.50",
|
||||||
# "mlx-vlm",
|
# "torch",
|
||||||
# "pillow",
|
# "pillow",
|
||||||
# "requests",
|
# "requests",
|
||||||
# "argparse",
|
# "argparse",
|
||||||
# "pdf2image",
|
# "pdf2image",
|
||||||
|
# "docling_core",
|
||||||
# ]
|
# ]
|
||||||
# ///
|
# ///
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import re
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
import requests
|
import requests
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from pdf2image import convert_from_bytes
|
from pdf2image import convert_from_bytes
|
||||||
from docling_core.types.doc import ImageRefMode
|
import torch
|
||||||
from docling_core.types.doc.document import DocTagsDocument, DoclingDocument
|
from transformers import AutoProcessor, AutoModelForVision2Seq
|
||||||
|
from docling_core.types.doc import DoclingDocument
|
||||||
|
from docling_core.types.doc.document import DocTagsDocument
|
||||||
|
|
||||||
# Add parent directory to path for imports
|
# Add parent directory to path for imports
|
||||||
import sys
|
import sys
|
||||||
sys.path.append(str(Path(__file__).parent.parent.parent))
|
sys.path.append(str(Path(__file__).parent.parent.parent))
|
||||||
|
|
||||||
from backend.utils import ensure_results_folder, load_pdf_page, get_project_root
|
from backend.utils import ensure_results_folder, load_pdf_page
|
||||||
from backend.config import MODEL_PATH, MAX_TOKENS, DEFAULT_DPI
|
from backend.config import MODEL_PATH, MAX_TOKENS, DEFAULT_DPI
|
||||||
|
|
||||||
def parse_arguments():
|
def parse_arguments():
|
||||||
"""Parse command line arguments."""
|
|
||||||
results_dir = ensure_results_folder()
|
results_dir = ensure_results_folder()
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description='Convert an image or PDF to docling format')
|
parser = argparse.ArgumentParser(description='Convert an image or PDF to docling format')
|
||||||
|
|
@ -52,7 +53,6 @@ def parse_arguments():
|
||||||
return parser.parse_args()
|
return parser.parse_args()
|
||||||
|
|
||||||
def load_image(image_path, page_num=1, dpi=DEFAULT_DPI):
|
def load_image(image_path, page_num=1, dpi=DEFAULT_DPI):
|
||||||
"""Load image from URL, local image file, or PDF."""
|
|
||||||
if urlparse(image_path).scheme in ['http', 'https']:
|
if urlparse(image_path).scheme in ['http', 'https']:
|
||||||
response = requests.get(image_path, stream=True, timeout=10)
|
response = requests.get(image_path, stream=True, timeout=10)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
@ -62,104 +62,85 @@ def load_image(image_path, page_num=1, dpi=DEFAULT_DPI):
|
||||||
pdf_images = convert_from_bytes(response.content, dpi=dpi, first_page=page_num, last_page=page_num)
|
pdf_images = convert_from_bytes(response.content, dpi=dpi, first_page=page_num, last_page=page_num)
|
||||||
if not pdf_images:
|
if not pdf_images:
|
||||||
raise Exception(f"Could not extract page {page_num} from PDF")
|
raise Exception(f"Could not extract page {page_num} from PDF")
|
||||||
return pdf_images[0]
|
return pdf_images[0].convert("RGB")
|
||||||
else:
|
else:
|
||||||
return Image.open(response.raw)
|
return Image.open(response.raw).convert("RGB")
|
||||||
else:
|
else:
|
||||||
image_path = Path(image_path)
|
image_path = Path(image_path)
|
||||||
if not image_path.exists():
|
if not image_path.exists():
|
||||||
raise FileNotFoundError(f"File not found: {image_path}")
|
raise FileNotFoundError(f"File not found: {image_path}")
|
||||||
|
|
||||||
if image_path.suffix.lower() == '.pdf':
|
if image_path.suffix.lower() == '.pdf':
|
||||||
return load_pdf_page(str(image_path), page_num, dpi)
|
return load_pdf_page(str(image_path), page_num, dpi).convert("RGB")
|
||||||
else:
|
else:
|
||||||
return Image.open(image_path)
|
return Image.open(image_path).convert("RGB")
|
||||||
|
|
||||||
def process_page(model, processor, config, args, pil_image, page_num=1):
|
|
||||||
"""Process a single page from a PDF or image file."""
|
|
||||||
from mlx_vlm.prompt_utils import apply_chat_template
|
|
||||||
from mlx_vlm.utils import stream_generate
|
|
||||||
|
|
||||||
|
def process_page(model, processor, args, pil_image, page_num=1):
|
||||||
results_dir = ensure_results_folder()
|
results_dir = ensure_results_folder()
|
||||||
|
|
||||||
# For web interface, always use output.doctags.txt
|
|
||||||
# For command line with specific pages, use page-specific names
|
|
||||||
if args.start_page == args.end_page and args.start_page == page_num:
|
if args.start_page == args.end_page and args.start_page == page_num:
|
||||||
# Single page processing
|
|
||||||
output_path = results_dir / "output.html"
|
|
||||||
doctags_path = results_dir / "output.doctags.txt"
|
doctags_path = results_dir / "output.doctags.txt"
|
||||||
|
output_path = results_dir / "output.html"
|
||||||
else:
|
else:
|
||||||
# Multi-page processing
|
|
||||||
output_path = results_dir / f"output_page{page_num}.html"
|
|
||||||
doctags_path = results_dir / f"output_page{page_num}.doctags.txt"
|
doctags_path = results_dir / f"output_page{page_num}.doctags.txt"
|
||||||
|
output_path = results_dir / f"output_page{page_num}.html"
|
||||||
|
|
||||||
print(f"Processing page {page_num}")
|
print(f"Processing page {page_num}")
|
||||||
|
|
||||||
# Save image temporarily
|
# Préparer les messages
|
||||||
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as temp_img_file:
|
messages = [
|
||||||
temp_img_path = temp_img_file.name
|
{
|
||||||
pil_image.save(temp_img_path, format='PNG')
|
"role": "user",
|
||||||
|
"content": [
|
||||||
|
{"type": "image"},
|
||||||
|
{"type": "text", "text": args.prompt}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
try:
|
prompt = processor.apply_chat_template(messages, add_generation_prompt=True)
|
||||||
# Apply chat template and generate
|
|
||||||
formatted_prompt = apply_chat_template(processor, config, args.prompt, num_images=1)
|
|
||||||
|
|
||||||
print(f"Generating DocTags for page {page_num}: \n\n")
|
device = next(model.parameters()).device
|
||||||
output = ""
|
inputs = processor(text=prompt, images=[pil_image], return_tensors="pt").to(device)
|
||||||
for token in stream_generate(
|
|
||||||
model, processor, formatted_prompt, [temp_img_path], max_tokens=MAX_TOKENS, verbose=False
|
|
||||||
):
|
|
||||||
output += token.text
|
|
||||||
print(token.text, end="")
|
|
||||||
if "</doctag>" in token.text:
|
|
||||||
break
|
|
||||||
print("\n\n")
|
|
||||||
|
|
||||||
finally:
|
# Génération
|
||||||
# Clean up temporary file
|
generated_ids = model.generate(**inputs, max_new_tokens=MAX_TOKENS)
|
||||||
if os.path.exists(temp_img_path):
|
prompt_length = inputs.input_ids.shape[1]
|
||||||
os.unlink(temp_img_path)
|
trimmed_generated_ids = generated_ids[:, prompt_length:]
|
||||||
|
|
||||||
# Save DocTags output
|
doctags = processor.batch_decode(trimmed_generated_ids, skip_special_tokens=False)[0].lstrip()
|
||||||
with open(doctags_path, 'w', encoding='utf-8') as f:
|
with open(doctags_path, "w", encoding="utf-8") as f:
|
||||||
f.write(output)
|
f.write(doctags)
|
||||||
print(f"Raw DocTags saved to: {doctags_path}")
|
print(f"DocTags saved to {doctags_path}")
|
||||||
|
|
||||||
|
doctags_doc = DocTagsDocument.from_doctags_and_image_pairs([doctags], [pil_image])
|
||||||
|
doc = DoclingDocument.load_from_doctags(doctags_doc, document_name=f"Page {page_num}")
|
||||||
|
html = doc.export_to_html()
|
||||||
|
|
||||||
|
with open(output_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(html)
|
||||||
|
print(f"HTML exported to {output_path}")
|
||||||
|
|
||||||
return output_path
|
return output_path
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
args = parse_arguments()
|
args = parse_arguments()
|
||||||
|
print("Loading model and processor...")
|
||||||
|
|
||||||
# Load the model
|
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
||||||
print("Loading model...")
|
model = AutoModelForVision2Seq.from_pretrained(
|
||||||
try:
|
MODEL_PATH,
|
||||||
from mlx_vlm import load
|
torch_dtype=torch.bfloat16,
|
||||||
from mlx_vlm.utils import load_config
|
_attn_implementation="flash_attention_2" if device.type == "cuda" else "eager"
|
||||||
|
).to(device)
|
||||||
|
processor = AutoProcessor.from_pretrained(MODEL_PATH)
|
||||||
|
|
||||||
model, processor = load(MODEL_PATH)
|
start_page = args.start_page
|
||||||
config = load_config(MODEL_PATH)
|
end_page = args.end_page or args.page
|
||||||
except Exception as e:
|
|
||||||
print(f"Error loading model: {e}")
|
|
||||||
return
|
|
||||||
|
|
||||||
# Process the image/PDF
|
for page_num in range(start_page, end_page + 1):
|
||||||
try:
|
pil_image = load_image(args.image, page_num=page_num, dpi=args.dpi)
|
||||||
# Handle single page or range
|
process_page(model, processor, args, pil_image, page_num)
|
||||||
start_page = args.start_page
|
|
||||||
end_page = args.end_page or args.page
|
|
||||||
|
|
||||||
for page_num in range(start_page, end_page + 1):
|
|
||||||
print(f"\nProcessing page {page_num}...")
|
|
||||||
|
|
||||||
pil_image = load_image(args.image, page_num=page_num, dpi=args.dpi)
|
|
||||||
print(f"Page {page_num} loaded: {pil_image.size}")
|
|
||||||
|
|
||||||
process_page(model, processor, config, args, pil_image, page_num)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error processing: {e}")
|
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
8
backend/requirements.txt
Normal file
8
backend/requirements.txt
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
transformers
|
||||||
|
accelerate
|
||||||
|
torch
|
||||||
|
torchvision
|
||||||
|
pdf2image
|
||||||
|
pillow
|
||||||
|
requests
|
||||||
|
flask
|
||||||
7
docker-compose.yml
Normal file
7
docker-compose.yml
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
services:
|
||||||
|
analyser:
|
||||||
|
image: ddd
|
||||||
|
ports:
|
||||||
|
- "8080:5000"
|
||||||
|
volumes:
|
||||||
|
- ./input:/
|
||||||
Loading…
Reference in a new issue