import os
import uuid
import base64
import re
import threading
import time
from typing import List, Dict
import logging
import tempfile
import shutil
import json
import asyncio
import hashlib
from openai import AsyncOpenAI
from readability import Document
import instructor
from fastapi import FastAPI, File, UploadFile, HTTPException, BackgroundTasks
from fastapi.responses import FileResponse, JSONResponse
import pypandoc
import fitz # PyMuPDF
from bs4 import BeautifulSoup, Comment
try:
from pptx import Presentation
from pptx.enum.shapes import MSO_SHAPE_TYPE
except ImportError:
pass
try:
import textract
except ImportError:
pass
logging.basicConfig(level=logging.DEBUG)
app = FastAPI()
client = instructor.apatch(AsyncOpenAI())
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
JOBS_DIR = os.path.join(tempfile.gettempdir(), 'jobs')
if not os.path.exists(JOBS_DIR):
os.makedirs(JOBS_DIR)
FORMAT_MAP = {
'.odt': 'odt',
'.pdf': 'pdf',
'.docx': 'docx',
'.html': 'html',
'.htm': 'html',
'.md': 'markdown',
'.txt': 'markdown',
'.rtf': 'rtf',
'.epub': 'epub',
'.xml': 'xml',
'.org': 'org',
'.commonmark': 'commonmark',
'.cm': 'commonmark',
'.wiki': 'mediawiki',
'.opml': 'opml'
}
ALLOWED_EXTENSIONS_FOR_ACCESSIBILITY = list(FORMAT_MAP.keys()) + ['.doc', '.ppt', '.pptx']
def get_pandoc_format(extension: str) -> str:
return FORMAT_MAP.get(extension, 'auto')
def update_job_status(job_id: str, status: str, message: str = '', result_file: str = None):
job_dir = os.path.join(JOBS_DIR, job_id)
status_file = os.path.join(job_dir, 'status.json')
status_data = {
'status': status,
'message': message,
'updated_at': time.time()
}
if result_file:
status_data['result_file'] = result_file
with open(status_file, 'w') as f:
json.dump(status_data, f)
def get_job_status(job_id: str):
job_dir = os.path.join(JOBS_DIR, job_id)
status_file = os.path.join(job_dir, 'status.json')
if not os.path.exists(status_file):
return None
with open(status_file, 'r') as f:
status_data = json.load(f)
return status_data
def delete_files_after_delay(file_paths: List[str], delay: int = 6000):
def delayed_delete():
time.sleep(delay)
for file_path in file_paths:
try:
if os.path.exists(file_path):
os.remove(file_path)
logging.debug(f"Fichier temporaire supprimé après délai : {file_path}")
except Exception as e:
logging.error(f"Erreur lors de la suppression du fichier {file_path} : {str(e)}")
thread = threading.Thread(target=delayed_delete)
thread.start()
def insert_page_comments_every_15_paragraphs(html_content: str) -> str:
soup = BeautifulSoup(html_content, 'html.parser')
paragraphs = soup.find_all('p')
page_number = 1
for i, p in enumerate(paragraphs, start=1):
if i % 15 == 1:
comment = soup.new_string(f"")
p.insert_before(comment)
page_number += 1
return str(soup)
def insert_css_into_html(html_content: str) -> str:
css_code = """
:root {
--font-size-min: 1rem;
--font-size-base: 1rem;
--font-size-large: 2.5rem;
--line-height: 1.5;
--font-family: Arial, Calibri, Verdana, sans-serif;
--text-color: #1a1a1a;
--background-color: #fdfdfd;
--link-color: #1a1a1a;
--heading-color-primary: Navy;
--heading-color-secondary: DarkGreen;
--heading-color-tertiary: DarkRed;
--heading-color-quaternary: DarkSlateGray;
--heading-color-cinq: DarkSlateBlue;
--heading-color-six: DarkViolet;
}
* {
font-size: 1rem;
}
html {
font-family: var(--font-family);
font-size: var(--font-size-base);
line-height: var(--line-height);
color: var(--text-color);
background-color: var(--background-color);
font-size: clamp(var(--font-size-min), 2vw, 1.5rem);
}
body {
margin: 20px auto;
max-width: 36em;
padding: 2rem;
hyphens: auto;
overflow-wrap: break-word;
text-rendering: optimizeLegibility;
font-kerning: normal;
text-align: left;
}
h1 {margin-left: 0; color: var(--heading-color-primary);}
h2 {margin-left: 1rem; color: var(--heading-color-secondary);}
h3 {margin-left: 2rem; color: var(--heading-color-tertiary);}
h4 {margin-left: 3rem; color: var(--heading-color-quaternary);}
h5 {margin-left: 4rem; color: var(--heading-color-cinq);}
h6 {margin-left: 5rem; color: var(--heading-color-six);}
@media (max-width: 600px) {
html {
font-size: clamp(var(--font-size-min), 4vw, 1.5rem);
}
body {
padding: 1rem;
}
h1 {font-size: clamp(1.5rem, 6vw, 2.5rem);}
h2 {font-size: clamp(1.25rem, 5vw, 2rem);}
h3 {font-size: clamp(1.125rem, 4.5vw, 1.75rem);}
h4, h5, h6 {font-size: clamp(1rem, 4vw, 1.5rem);}
}
@media print {
body {
background-color: transparent;
color: black;
font-size: 12pt;
}
p, h2, h3 {
orphans: 3;
widows: 3;
}
h2, h3, h4 {
page-break-after: avoid;
}
}
p {margin: 1em 0; font-size: 1rem;}
a {color: var(--link-color); text-decoration: none;}
a:visited {color: var(--link-color);}
a:hover, a:focus {text-decoration: underline;}
img {max-width: 100%; height: auto;}
table {
margin: 1em 0;
border-collapse: collapse;
width: 100%;
overflow-x: auto;
display: block;
font-variant-numeric: lining-nums tabular-nums;
}
table caption {margin-bottom: 0.75em;}
th, td {border: 1px solid #000; padding: 0.5em; text-align: left;}
tbody tr:nth-child(odd) {background-color: #f2f2f2;}
tbody tr:nth-child(even) {background-color: #ffffff;}
blockquote {
margin: 1em 0 1em 1.7em;
padding-left: 1em;
border-left: 2px solid #e6e6e6;
color: #606060;
}
code {
font-family: Menlo, Monaco, 'Lucida Console', Consolas, monospace;
font-size: 0.85rem;
margin: 0;
white-space: pre-wrap;
}
pre {
margin: 1em 0;
overflow: auto;
}
pre code {
padding: 0;
overflow: visible;
overflow-wrap: normal;
}
.sourceCode {
background-color: transparent;
overflow: visible;
}
hr {
background-color: #1a1a1a;
border: none;
height: 1px;
margin: 1em 0;
}
span.smallcaps {font-variant: small-caps;}
span.underline {text-decoration: underline;}
div.column {display: inline-block; vertical-align: top; width: 50%;}
.description {
background-color: #f0f3ff;
padding: 1em;
border: 1px solid black;
}
div.hanging-indent {
margin-left: 1.5em;
text-indent: -1.5em;
}
ul.task-list {list-style: none;}
.display.math {
display: block;
text-align: center;
margin: 0.5rem auto;
}
"""
final_soup = BeautifulSoup(html_content, 'html.parser')
style_tag = final_soup.new_tag('style')
style_tag.string = css_code
head_tag = final_soup.head
if head_tag:
head_tag.clear()
head_tag.append(style_tag)
else:
head_tag = final_soup.new_tag('head')
head_tag.append(style_tag)
final_soup.insert(0, head_tag)
final_html = str(final_soup)
return final_html
def encode_image_from_data_uri(data_uri: str) -> str:
try:
header, encoded = data_uri.split(',', 1)
encoded = ''.join(encoded.split())
return encoded
except Exception as e:
logging.error(f"Erreur lors de l'encodage de l'image : {str(e)}")
return ""
def pdf_to_html(input_filename: str) -> str:
soup = BeautifulSoup("
", 'html.parser')
body = soup.body
page_number = 1
with fitz.open(input_filename) as doc:
for page in doc:
page_comment = f""
body.append(BeautifulSoup(page_comment, 'html.parser'))
page_html = page.get_text("html")
page_fragment = BeautifulSoup(page_html, 'html.parser')
body.append(page_fragment)
page_number += 1
return str(soup)
def convert_with_pandoc(input_filename: str, input_format: str) -> str:
if input_format == 'docx':
try:
output = pypandoc.convert_file(
input_filename,
'html',
format=input_format,
outputfile=None,
extra_args=['--self-contained', '--strip-comments', '--quiet']
)
return output
except RuntimeError as e:
logging.error(f"Pandoc a rencontré une erreur avec --self-contained sur un docx : {str(e)}")
raise RuntimeError("Impossible de convertir le docx avec --self-contained. Les images ne peuvent pas être traitées.")
elif os.path.splitext(input_filename)[1].lower() == '.ppt':
try:
output = pypandoc.convert_file(
input_filename,
'html',
format='auto',
outputfile=None,
extra_args=['--strip-comments', '--quiet']
)
return output
except RuntimeError as e:
logging.error(f"Pandoc a rencontré une erreur avec le format 'auto' sur un ppt : {str(e)}, tentative avec 'ppt'.")
try:
output = pypandoc.convert_file(
input_filename,
'html',
format='ppt',
outputfile=None,
extra_args=['--strip-comments', '--quiet']
)
return output
except RuntimeError as e:
logging.error(f"Pandoc a rencontré une erreur avec le format 'ppt' sur un ppt : {str(e)}.")
raise
else:
try:
output = pypandoc.convert_file(
input_filename,
'html',
format=input_format,
outputfile=None,
extra_args=['--self-contained', '--strip-comments', '--quiet']
)
return output
except RuntimeError as e:
logging.error(f"Pandoc a rencontré une erreur : {str(e)}, tentative sans --self-contained.")
output = pypandoc.convert_file(
input_filename,
'html',
format=input_format,
outputfile=None,
extra_args=['--strip-comments', '--quiet']
)
return output
def text_to_html(text: str) -> str:
lines = text.split('\n')
html_lines = ['' + line.strip() + '
' for line in lines if line.strip()]
return "" + "\n".join(html_lines) + ""
def convert_ppt_to_text(input_filename: str) -> str:
if 'Presentation' not in globals():
raise HTTPException(status_code=500, detail="La librairie python-pptx n'est pas installée.")
prs = Presentation(input_filename)
text_content = []
for slide in prs.slides:
for shape in slide.shapes:
if hasattr(shape, "text"):
text_content.append(shape.text)
return "\n".join(text_content)
def convert_pptx_to_html(input_filename: str) -> str:
if 'Presentation' not in globals():
raise HTTPException(status_code=500, detail="La librairie python-pptx n'est pas installée.")
prs = Presentation(input_filename)
html_content = ""
slide_number = 1
for slide in prs.slides:
html_content += f""
for shape in slide.shapes:
if shape.has_text_frame:
text_content = shape.text_frame.text
# Basic handling for different text levels - can be improved
if shape.is_placeholder:
if shape.placeholder_format.idx == 0: # Title
html_content += f"{text_content}
"
elif shape.placeholder_format.idx == 1: # Subtitle/Content
html_content += f"{text_content}
"
else:
html_content += f"{text_content}
"
else:
html_content += f"{text_content}
"
elif shape.shape_type == MSO_SHAPE_TYPE.PICTURE:
image = shape.image
image_bytes = image.blob
base64_encoded = base64.b64encode(image_bytes).decode('utf-8')
mime_type = image.content_type
html_content += f'
'
slide_number += 1
html_content += ""
return html_content
def convert_doc_to_text(input_filename: str) -> str:
if 'textract' not in globals():
raise HTTPException(status_code=500, detail="La librairie textract n'est pas installée.")
text = textract.process(input_filename).decode('utf-8', errors='replace')
return text
def clean_html_file(input_filepath: str, cleaned_output_filepath: str) -> bool:
try:
with open(input_filepath, 'r', encoding='utf-8') as f:
html_content = f.read()
doc = Document(html_content)
cleaned_html = doc.summary()
with open(cleaned_output_filepath, 'w', encoding='utf-8') as f:
f.write(cleaned_html)
logging.debug("Contenu HTML nettoyé avec readability-lxml.")
return True
except Exception as e:
logging.error(f"Erreur lors du nettoyage du fichier HTML {input_filepath} : {str(e)}")
return False
from bs4 import Comment
async def clean_html_content(html_content: str, image_counter: List[int], images_data: Dict[str, Dict[str, str]]) -> str:
soup = BeautifulSoup(html_content, 'html.parser')
logging.debug(f"DEBUG CLEAN_HTML: Début de clean_html_content")
for tag in soup.find_all():
if 'style' in tag.attrs:
del tag['style']
for element in soup.find_all(['header', 'footer']):
element.decompose()
for span in soup.find_all('span'):
span.unwrap()
img_tags = soup.find_all('img')
logging.debug(f"DEBUG CLEAN_HTML: Nombre de balises
trouvées : {len(img_tags)}")
if img_tags:
if len(img_tags) > 40:
logging.warning(f"Number of images ({len(img_tags)}) exceeds 40. Images will be ignored.")
for img in img_tags:
img.decompose()
else:
for img in img_tags:
src = img.get('src', '')
logging.debug(f"DEBUG CLEAN_HTML: Traitement de la balise
avec src='{src[:100]}...'")
X = image_counter[0]
if src.startswith('data:image/'):
logging.debug(f"DEBUG CLEAN_HTML: src commence par data:image/")
base64_image = encode_image_from_data_uri(src)
if base64_image:
images_data[f"IMG_{X}"] = {
'base64_image': base64_image
}
comment_tag = Comment(f"IMG_{X}")
img.insert_before(comment_tag)
logging.debug(f"DEBUG CLEAN_HTML: Insertion du commentaire avant l'image : {comment_tag}")
logging.debug(f"DEBUG CLEAN_HTML: Vérification immédiate du commentaire après insertion : {soup.find(string=comment_tag)}")
img.decompose()
logging.debug(f"DEBUG CLEAN_HTML: Suppression de la balise img.")
image_counter[0] += 1
else:
logging.debug(f"DEBUG CLEAN_HTML: Erreur lors de l'encodage base64, suppression de l'image.")
img.decompose()
else:
logging.debug(f"DEBUG CLEAN_HTML: src ne commence PAS par data:image/, suppression de l'image.")
img.decompose()
else:
logging.debug("DEBUG CLEAN_HTML: Aucune balise
trouvée dans le contenu HTML.")
logging.debug(f"DEBUG CLEAN_HTML: Fin de clean_html_content")
return str(soup)
def reinsert_images(html_content: str, images_data: Dict[str, Dict[str, str]]) -> str:
soup = BeautifulSoup(html_content, 'html.parser')
for comment in soup.find_all(string=lambda text: isinstance(text, Comment)):
match = re.match(r'IMG_(\d+)', comment)
if match:
image_number = match.group(1)
image_key = f"IMG_{image_number}"
if image_key in images_data:
img_tag = soup.new_tag('img')
img_tag['src'] = f"data:image/jpeg;base64,{images_data[image_key]['base64_image']}"
img_tag['alt'] = images_data[image_key].get('description', 'Description indisponible')
new_content = soup.new_tag('div', attrs={'class': 'image-block'})
new_content.append(img_tag)
p_tag = soup.new_tag('p', attrs={'class': 'description'})
description = images_data[image_key]['description'].replace('\n', ' ').strip()
description = re.sub(r'\s+', ' ', description)
p_tag.string = f"Image {image_number} : {description}"
new_content.append(p_tag)
# Ajout d'un journal pour vérifier l'insertion
logging.debug(f"Inserting description for {image_key}: {p_tag.get_text(strip=True)}")
comment.replace_with(new_content)
else:
logging.error(f"Données pour {image_key} non trouvées.")
return str(soup)
def get_context_for_image(html_content: str, image_key: str) -> str:
index = html_content.find(f"")
if index == -1:
return ""
start_context = max(0, index - 500)
context_snippet = html_content[start_context:index]
snippet_soup = BeautifulSoup(context_snippet, 'html.parser')
context_text = snippet_soup.get_text(separator=' ', strip=True)
return context_text
async def get_image_description(base64_image: str, prompt: str) -> str:
try:
response = await client.chat.completions.create(
model="gpt-4o-mini",
messages=[
{
"role": "user",
"content": [
{
"type": "text",
"text": prompt,
},
{
"type": "image_url",
"image_url": {
"url": f"data:image/jpeg;base64,{base64_image}"
},
},
],
}
],
)
description = response.choices[0].message.content.strip()
return description
except Exception as e:
logging.error(f"Erreur lors de l'appel à l'API OpenAI : {str(e)}")
return "Description indisponible."
async def rewrite_html_accessible(html_content: str) -> str:
prompt = (
"Je vais te donner un fichier HTML, et je voudrais que tu le réécrives pour permettre l'accessibilité à toutes les formes de handicap, tout en **préservant strictement l'ordre du contenu original**.\n"
"Commence à analyser le plan du document. Il faut d'abord identifier les titres et comprendre leur logique :\n"
"- A priori, les titres qui sont préfixés par une écriture romaine (I, II, III), "
"par un nombre (1, 2, 3) ou par une lettre (a, b, c, ou bien A, B, C) doivent être de même niveau."
"Idem pour les titres rédigés en majuscules.\n"
"- Quand une expression très courte qui ne ressemble pas syntaxiquement à une phrase est présentée sur une seule ligne,"
"il y a des chances qu'il s'agisse d'un titre : dans ce cas (et si c'est pertinent) traite-la comme telle.\n"
"- Au contraire, **une phrase longue ne doit JAMAIS être traitée comme un titre**,"
"même quand elle est précédée par un numéro ou une lettre."
"De même, ne traite jamais comme un titre un ensemble de plusieurs phrases. Je repète : les balises , , etc., ne sont destinées qu'à encadrer des expressions relativement courtes, et rien d'autre.\n\n"
"Tu ne dois **rien réorganiser**, **ne rien supprimer** et **ne rien ajouter** en termes de structure ou de contenu. "
"Ton intervention doit se faire exclusivement sur la **forme** du document : le contenu doit être **intégralement préservé dans le même ordre**, jusqu'à la fin. "
"Laisse la balise vide.\n"
"IMPORTANT : Tu dois **respecter scrupuleusement l'ordre indiqué par les commentaires HTML de la forme ,** s'ils existent. On doit avoir [...] [...] [...], et ainsi de suite, dans l'ordre exact et sans en oublier un seul. C'est très important ! Ces marqueurs te permettent de t'assurer que la page est bien retranscrite dans le bon ordre. Ne déplace, ne supprime, et ne modifie pas ces commentaires.\n"
"Attention, ce document est peut-être issu d'un PDF ou d'un DOCX. Il faut donc être attentif :\n"
"- Aux balises
qui suivent immédiatement les marqueurs : il peut s'agir de headers. Pour le savoir, il faut les comparer entre eux pour savoir s'ils sont à peu près similaires.\n"
"- Aux balises
qui précèdent immédiatement les marqueurs : il peut s'agir de footers. De même, il faut les comparer entre eux pour savoir s'ils sont à peu près similaires.\n"
"Dans tous les cas, il faut supprimer les balises
correspondant aux headers et les footers identifiés. Attention, ces suppressions ne doivent pas affecter les autres éléments.\n"
"S'il y a des retours à la ligne injustifiés, il faut rétablir l'intégrité des phrases, et constituer de véritables paragraphes complets. L'ensemble du code doit être inclus entre des balises \n"
"Tu donneras la totalité du HTML réécrit, et rien d'autre, ni avant ni après. "
"Ne résume jamais les informations, ne réorganise pas le contenu et ne supprime aucune section.\n\n"
"Voici tout d'abord les règles à suivre pour avoir un document accessible :\n\n"
"1. Limiter l'italique et les soulignements.\n"
"2. S'il y a des tableaux, insérer un tiret dans les cellules ne contenant pas d’information, et associer une légende aux tableaux.\n"
"3. Pour les titres, utilise absolument les balises h1, h2, h3, h4, h5 et h6. Utilise la balise h1 pour le titre qui a le plus grand niveau.\n\n"
"On évite les balises
et - \n"
"Encore une fois, fais bien attention à reproduire fidèlement l'ordre des marqueurs , dans l'ordre croissant des X.\n"
"N'oublie pas qu'on ne doit avoir AUCUN header et AUCUN footer.\n"
"Voici maintenant le fichier HTML d'origine :\n"
+ html_content
)
try:
response = await client.chat.completions.create(
model="o1-mini",
messages=[
{"role": "user", "content": prompt}
],
)
rewritten_html = response.choices[0].message.content.strip()
rewritten_html = rewritten_html.replace("<!--", "")
return rewritten_html
except Exception as e:
logging.error(f"Erreur lors de la réécriture du HTML : {str(e)}")
return html_content
@app.post("/accessibility/")
async def convert_file_to_html(
file: UploadFile = File(...),
background_tasks: BackgroundTasks = BackgroundTasks()
):
try:
job_id = str(uuid.uuid4())
job_dir = os.path.join(JOBS_DIR, job_id)
os.makedirs(job_dir)
ext = os.path.splitext(file.filename)[1].lower()
if ext not in ALLOWED_EXTENSIONS_FOR_ACCESSIBILITY:
raise HTTPException(status_code=400, detail=f"Extension de fichier non supportée : {ext}")
input_file_path = os.path.join(job_dir, f'input{ext}')
with open(input_file_path, "wb") as f:
shutil.copyfileobj(file.file, f)
status = {
'status': 'pending',
'message': 'Traitement démarré',
'created_at': time.time()
}
status_file = os.path.join(job_dir, 'status.json')
with open(status_file, 'w') as f:
json.dump(status, f)
background_tasks.add_task(process_file_accessibility, job_id, input_file_path, ext, file.filename)
return JSONResponse(content={'job_id': job_id})
except Exception as e:
logging.error(f"Erreur lors du démarrage du job : {str(e)}")
return JSONResponse(status_code=500, content={"message": f"Erreur lors du démarrage du job : {str(e)}"})
async def process_file_accessibility(job_id: str, input_file_path: str, ext: str, original_filename: str):
job_dir = os.path.join(JOBS_DIR, job_id)
try:
update_job_status(job_id, 'processing', 'Le fichier est en cours de traitement')
base_filename = os.path.splitext(original_filename)[0]
# Conversion en HTML sans extraire les images à ce stade
if ext == '.pdf':
html_content = pdf_to_html(input_file_path)
elif ext == '.pptx':
html_content = convert_pptx_to_html(input_file_path)
html_content = insert_page_comments_every_15_paragraphs(html_content)
elif ext == '.ppt':
text = convert_ppt_to_text(input_file_path)
html_content = text_to_html(text)
html_content = insert_page_comments_every_15_paragraphs(html_content)
elif ext == '.doc':
text = convert_doc_to_text(input_file_path)
html_content = text_to_html(text)
html_content = insert_page_comments_every_15_paragraphs(html_content)
logging.debug(f"DEBUG ACCESSIBILITY (.docx): HTML après conversion Pandoc : {html_content[:500]}...")
elif ext in ['.html', '.htm']:
with open(input_file_path, 'r', encoding='utf-8') as f:
raw_html = f.read()
try:
doc = Document(raw_html)
cleaned = doc.summary()
html_content = cleaned
except Exception as e:
logging.error(f"Erreur lors du nettoyage HTML : {str(e)}")
html_content = raw_html
html_content = insert_page_comments_every_15_paragraphs(html_content)
else:
input_format = get_pandoc_format(ext)
html_content = convert_with_pandoc(input_file_path, input_format)
html_content = insert_page_comments_every_15_paragraphs(html_content)
if ext == '.docx':
logging.debug(f"DEBUG ACCESSIBILITY (.docx): HTML après conversion Pandoc : {html_content[:500]}...")
# Nettoyage et extraction des images
image_counter = [1]
images_data = {}
logging.debug(f"DEBUG ACCESSIBILITY: HTML avant clean_html_content : {html_content[:500]}...")
cleaned_html = await clean_html_content(html_content, image_counter, images_data)
logging.debug(f"DEBUG ACCESSIBILITY: HTML après clean_html_content : {cleaned_html}...")
logging.debug(f"DEBUG ACCESSIBILITY: images_data après clean_html_content : {images_data}")
if ext == '.docx':
logging.debug(f"DEBUG ACCESSIBILITY (.docx): HTML après clean_html_content (complet) : {cleaned_html}") # Afficher le HTML complet
# Décrire les images
for image_key in images_data:
context = get_context_for_image(cleaned_html, image_key)
prompt = (
"Décris ce que l'on peut voir sur cette image, pour qu'un lecteur malvoyant puisse comprendre ce qu'elle représente."
"\nJe vais maintenant te donner les dernières phrases qui précèdent cette image. "
"Prends en compte ce contexte pour l'interpréter :\n\"" + context + "\""
)
images_data[image_key]['prompt'] = prompt
tasks = []
for image_key in images_data:
base64_image = images_data[image_key]['base64_image']
prompt = images_data[image_key]['prompt']
tasks.append((image_key, asyncio.create_task(get_image_description(base64_image, prompt))))
results = await asyncio.gather(*(t[1] for t in tasks))
for (image_key, _), description in zip(tasks, results):
images_data[image_key]['description'] = description
# Réécriture accessible
rewritten_html = await rewrite_html_accessible(cleaned_html)
# Réinsertion des images
final_html = reinsert_images(rewritten_html, images_data)
# Nettoyage final
final_soup = BeautifulSoup(final_html, 'html.parser')
scripts_to_remove = final_soup.find_all('script', src=True)
for script in scripts_to_remove:
if script['src'].startswith('https://bentham-converttohtml.hf.space/'):
script.decompose()
final_html = str(final_soup)
# Supprimer lignes contenant ```
final_html = re.sub(r'^\s*```(?:html)?\s*$', '', final_html, flags=re.MULTILINE)
# Insérer le CSS
final_html = insert_css_into_html(final_html)
output_filename = os.path.join(job_dir, f"{base_filename}.html")
with open(output_filename, 'w', encoding='utf-8') as f:
f.write(final_html)
update_job_status(job_id, 'completed', 'Traitement terminé', result_file=f"{base_filename}.html")
delete_files_after_delay([input_file_path], delay=6000)
except Exception as e:
logging.error(f"Erreur lors du traitement du job {job_id}: {str(e)}")
update_job_status(job_id, 'error', f"Erreur: {str(e)}")
@app.get("/status/{job_id}")
async def check_status(job_id: str):
status_data = get_job_status(job_id)
if status_data is None:
return JSONResponse(status_code=404, content={"message": "Job non trouvé"})
return JSONResponse(content=status_data)
@app.get("/result/{job_id}")
async def get_result(job_id: str):
job_dir = os.path.join(JOBS_DIR, job_id)
status_data = get_job_status(job_id)
if status_data is None:
return JSONResponse(status_code=404, content={"message": "Job non trouvé"})
if status_data.get('status') != 'completed':
return JSONResponse(status_code=400, content={"message": "Résultat non prêt"})
result_file = status_data.get('result_file')
if not result_file:
return JSONResponse(status_code=500, content={"message": "Fichier résultat non trouvé"})
result_file_path = os.path.join(job_dir, result_file)
if not os.path.exists(result_file_path):
return JSONResponse(status_code=500, content={"message": "Fichier résultat non trouvé sur le serveur"})
return FileResponse(result_file_path, filename=os.path.basename(result_file_path), media_type='text/html')
def delete_temp_files(file_paths: list):
for file_path in file_paths:
try:
if os.path.exists(file_path):
os.remove(file_path)
logging.debug(f"Fichier temporaire supprimé : {file_path}")
except Exception as e:
logging.error(f"Erreur lors de la suppression du fichier {file_path} : {str(e)}")
@app.post("/convert_to_txt/")
async def convert_file_to_txt(
file: UploadFile = File(...),
background_tasks: BackgroundTasks = BackgroundTasks()
):
try:
original_filename = file.filename
base_filename, ext = os.path.splitext(original_filename)
ext = ext.lower()
allowed_extensions = [
'.odt', '.pdf', '.docx', '.html', '.htm', '.md', '.txt', '.rtf', '.epub',
'.tex', '.xml', '.org', '.commonmark', '.cm', '.wiki', '.opml',
'.ppt', '.pptx', '.doc'
]
if ext not in allowed_extensions:
raise HTTPException(status_code=400, detail=f"Extension de fichier non supportée : {ext}")
with tempfile.NamedTemporaryFile(suffix=ext, delete=False) as input_tmp_file:
input_filename = input_tmp_file.name
with open(input_filename, "wb") as f:
shutil.copyfileobj(file.file, f)
# Nettoyage si HTML direct
if ext in ['.html', '.htm']:
cleaned_input_filename = input_filename + '_cleaned.html'
nettoyage_reussi = clean_html_file(input_filename, cleaned_input_filename)
if not nettoyage_reussi:
raise HTTPException(status_code=500, detail="Erreur lors du nettoyage du fichier HTML.")
input_filename = cleaned_input_filename
# Conversion en HTML via appropriate methods
if ext == '.pdf':
html_content = pdf_to_html(input_filename)
elif ext == '.pptx':
html_content = convert_pptx_to_html(input_filename)
elif ext == '.ppt':
try:
html_content = convert_with_pandoc(input_filename, get_pandoc_format(ext))
except Exception as e:
logging.error(f"Erreur lors de la conversion de .ppt avec pypandoc: {e}")
raise HTTPException(status_code=500, detail=f"Erreur lors de la conversion du fichier .ppt: {e}")
elif ext == '.doc':
text = convert_doc_to_text(input_filename)
html_content = text_to_html(text)
elif ext in ['.html', '.htm']:
html_content = convert_with_pandoc(input_filename, 'html')
else:
input_format = get_pandoc_format(ext)
html_content = convert_with_pandoc(input_filename, input_format)
if ext == '.docx':
logging.debug(f"DEBUG CONVERT_TO_TXT (.docx): HTML après conversion Pandoc : {html_content[:500]}...")
# Nettoyage et extraction des images
image_counter = [1]
images_data = {}
logging.debug(f"DEBUG CONVERT_TO_txt: HTML avant clean_html_content : {html_content[:500]}...")
cleaned_html = await clean_html_content(html_content, image_counter, images_data)
logging.debug(f"DEBUG CONVERT_TO_txt: HTML après clean_html_content : {cleaned_html}...")
logging.debug(f"DEBUG CONVERT_TO_txt: images_data après clean_html_content : {images_data}")
if ext == '.docx':
logging.debug(f"DEBUG CONVERT_TO_txt (.docx): HTML après clean_html_content (complet) : {cleaned_html}") # Afficher le HTML complet
# Vérification des commentaires IMG_X dans le HTML nettoyé
cleaned_soup = BeautifulSoup(cleaned_html, 'html.parser')
has_img_comments = any(re.match(r'IMG_\d+', str(comment)) for comment in cleaned_soup.find_all(string=lambda text: isinstance(text, Comment)))
logging.debug(f"DEBUG CONVERT_TO_txt: Présence de commentaires IMG_X après nettoyage : {has_img_comments}")
# Description des images pour le mode texte
for image_key in images_data:
context = get_context_for_image(cleaned_html, image_key)
prompt = (
"Cette image est incluse dans un cours. Je voudrais que tu me donnes les informations pertinentes qu'elle contient. "
"Ne commente pas les figures, les couleurs ni les formes.\n"
"- Si l'image ne contient que des textes, il faut simplement les retranscrire.\n"
"- Si l'image contient des schémas, expose ce qu'ils signifient, en phrases complètes, claires et compréhensibles.\n"
"Si l'image ne contient aucune information textuelle ou schématique, renvoie \"no-text\"."
"\nJe vais maintenant te donner les dernières phrases qui précèdent cette image. Il faut prendre en compte ce contexte:\n\""
+ context + " \" "
)
images_data[image_key]['prompt'] = prompt
tasks = []
for image_key, data in images_data.items():
base64_image = data['base64_image']
prompt = data['prompt']
tasks.append((image_key, asyncio.create_task(get_image_description(base64_image, prompt=prompt))))
results = await asyncio.gather(*(t[1] for t in tasks))
for (image_key, _), description in zip(tasks, results):
images_data[image_key]['description'] = description
# Réinsertion des images avec descriptions
logging.debug(f"DEBUG CONVERT_TO_txt: HTML avant reinsert_images : {cleaned_html[:500]}...")
logging.debug(f"DEBUG CONVERT_TO_txt: images_data avant reinsert_images : {images_data}")
final_html = reinsert_images(cleaned_html, images_data)
logging.debug(f"DEBUG CONVERT_TO_txt: HTML après reinsert_images : {final_html[:500]}...")
# Vérification des descriptions insérées
soup_final = BeautifulSoup(final_html, 'html.parser')
description_paragraphs = soup_final.find_all('p', class_='description')
logging.debug(f"DEBUG CONVERT_TO_txt: Nombre de descriptions insérées : {len(description_paragraphs)}")
for desc in description_paragraphs:
logging.debug(f"Description insérée : {desc.get_text(strip=True)}")
# Supprimer les images avec "no-text"
for div in soup_final.find_all('div', class_='image-block'):
desc_tag = div.find('p', class_='description')
if desc_tag and 'no-text' in desc_tag.get_text(strip=True):
div.decompose()
# Supprimer les balises