From b964fcb2e66bdff2c49ec6d5f0d894684f989470 Mon Sep 17 00:00:00 2001 From: urieljarethbusiness-cpu Date: Sun, 17 May 2026 11:18:12 -0600 Subject: [PATCH] 1 --- .env.example | 1 + AGENTS.md | 2 +- README.md | 1 + backend/app/config.py | 1 + .../app/services/providers/mistral_client.py | 28 +++++++----------- docker-compose.yaml | 14 +++++++-- estacion documentos.html | 29 +++++++------------ .../[subjectId]/weeks/[weekNumber]/page.tsx | 4 +-- 8 files changed, 40 insertions(+), 40 deletions(-) diff --git a/.env.example b/.env.example index d7c72b2..fe0065d 100644 --- a/.env.example +++ b/.env.example @@ -2,4 +2,5 @@ APP_ENV=development APP_DATA_DIR=./data DATABASE_PATH=./data/app.db MISTRAL_API_KEY= +MISTRAL_OCR_MODEL=mistral-ocr-latest DEEPGRAM_API_KEY= diff --git a/AGENTS.md b/AGENTS.md index 93db987..6bb42c1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -18,7 +18,7 @@ - `KnowledgeManager` owns filesystem layout and SQLite metadata; processors should not write final Markdown directly. - `IngestionService` selects processors by `source_type` and wraps output with Markdown frontmatter for LLM ingestion. - Text and Markdown are read directly; `.docx` uses `python-docx`; PDFs use PyMuPDF text extraction unless `use_ocr=true` is passed. PDF uploads can also pass `page_ranges` like `1-5,8,10-12`; ranges apply to both standard extraction and Mistral OCR. -- OCR uses Mistral `https://api.mistral.ai/v1/chat/completions` with model `pixtral-12b-2409`. +- OCR uses Mistral `https://api.mistral.ai/v1/ocr` with `MISTRAL_OCR_MODEL`, defaulting to `mistral-ocr-latest`. - Audio uses Deepgram `https://api.deepgram.com/v1/listen?model=nova-3&smart_format=true&language=es`. - Video processing requires `ffmpeg`; audio is extracted to `data/tmp/`, transcribed with Deepgram, then deleted. - The frontend calls the backend through `NEXT_PUBLIC_API_URL`; when ports change, update `frontend/.env.local` or use the adaptive `.bat`. diff --git a/README.md b/README.md index 0c1ec69..f641fe1 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,7 @@ Para Coolify, configura backend y frontend como servicios separados. Monta un vo - `APP_DATA_DIR`: carpeta de datos persistentes. - `DATABASE_PATH`: ruta SQLite. - `MISTRAL_API_KEY`: OCR para PDFs escaneados. +- `MISTRAL_OCR_MODEL`: modelo OCR de Mistral. Por defecto `mistral-ocr-latest`; puedes fijarlo a `mistral-ocr-2512`. - `DEEPGRAM_API_KEY`: transcripción de audio/video. No subas claves reales al repositorio. diff --git a/backend/app/config.py b/backend/app/config.py index 5ba5f01..58e1de0 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -8,6 +8,7 @@ class Settings(BaseSettings): app_data_dir: Path = Path("./data") database_path: Path = Path("./data/app.db") mistral_api_key: str = "" + mistral_ocr_model: str = "mistral-ocr-latest" deepgram_api_key: str = "" model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore") diff --git a/backend/app/services/providers/mistral_client.py b/backend/app/services/providers/mistral_client.py index ad524ec..24e5084 100644 --- a/backend/app/services/providers/mistral_client.py +++ b/backend/app/services/providers/mistral_client.py @@ -9,8 +9,7 @@ from backend.app.config import settings class MistralClient: - endpoint = "https://api.mistral.ai/v1/chat/completions" - model = "pixtral-12b-2409" + endpoint = "https://api.mistral.ai/v1/ocr" def ocr_image(self, image_path: Path) -> str: if not settings.mistral_api_key: @@ -23,20 +22,9 @@ class MistralClient: "Authorization": f"Bearer {settings.mistral_api_key}", }, json={ - "model": self.model, - "messages": [ - { - "role": "user", - "content": [ - { - "type": "text", - "text": "Extract all text from this image and format it as Markdown. If there are tables, represent them using Markdown table syntax. Do not include commentary.", - }, - {"type": "image_url", "image_url": data_url}, - ], - } - ], - "max_tokens": 3000, + "model": settings.mistral_ocr_model, + "document": {"type": "image_url", "image_url": data_url}, + "include_image_base64": False, }, timeout=180, ) @@ -45,7 +33,13 @@ class MistralClient: if not response.ok: raise RuntimeError(f"Mistral error {response.status_code}: {response.text}") data = response.json() - return data["choices"][0]["message"]["content"] + pages = data.get("pages") or [] + markdown_pages = [page.get("markdown", "").strip() for page in pages if page.get("markdown")] + if markdown_pages: + return "\n\n".join(markdown_pages) + if data.get("markdown"): + return data["markdown"].strip() + raise RuntimeError("Mistral OCR no devolvio Markdown") def _image_data_url(self, image_path: Path) -> str: encoded = base64.b64encode(image_path.read_bytes()).decode("ascii") diff --git a/docker-compose.yaml b/docker-compose.yaml index a8ab826..ca96d8a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -8,11 +8,14 @@ services: APP_DATA_DIR: /app/data DATABASE_PATH: /app/data/app.db MISTRAL_API_KEY: ${MISTRAL_API_KEY:-} + MISTRAL_OCR_MODEL: ${MISTRAL_OCR_MODEL:-mistral-ocr-latest} DEEPGRAM_API_KEY: ${DEEPGRAM_API_KEY:-} volumes: - station-data:/app/data expose: - "8000" + ports: + - "8000" restart: unless-stopped healthcheck: test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=5)"] @@ -34,10 +37,17 @@ services: HOSTNAME: 0.0.0.0 expose: - "3000" + ports: + - "3000" depends_on: - backend: - condition: service_healthy + - backend restart: unless-stopped + healthcheck: + test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:3000').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s volumes: station-data: diff --git a/estacion documentos.html b/estacion documentos.html index 80f4644..7bf5f8b 100644 --- a/estacion documentos.html +++ b/estacion documentos.html @@ -1306,28 +1306,19 @@ async function mistralOCR(dataUrl) { try { - const res = await fetch('https://api.mistral.ai/v1/chat/completions', { + const res = await fetch('https://api.mistral.ai/v1/ocr', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${MISTRAL_API_KEY}` }, body: JSON.stringify({ - model: 'pixtral-12b-2409', - messages: [{ - role: 'user', - content: [ - { - type: 'text', - text: 'Extract all text from this image and format it as Markdown. If there are tables, represent them using Markdown table syntax. Do not include any commentary or introductions, just the document content.' - }, - { - type: 'image_url', - image_url: dataUrl // dataUrl ya incluye 'data:image/jpeg;base64,...' - } - ] - }], - max_tokens: 2000 + model: 'mistral-ocr-latest', + document: { + type: 'image_url', + image_url: dataUrl // dataUrl ya incluye 'data:image/jpeg;base64,...' + }, + include_image_base64: false }) }); @@ -1342,9 +1333,11 @@ } const data = await res.json(); - if (!data.choices || !data.choices[0]) throw new Error("Respuesta de API vacía"); + const pages = data.pages || []; + const markdown = pages.map(page => page.markdown || '').filter(Boolean).join('\n\n'); + if (!markdown && !data.markdown) throw new Error("Respuesta de API vacía"); - return data.choices[0].message.content; + return markdown || data.markdown; } catch (error) { console.error("Mistral API Error:", error); throw error; diff --git a/frontend/app/subjects/[subjectId]/weeks/[weekNumber]/page.tsx b/frontend/app/subjects/[subjectId]/weeks/[weekNumber]/page.tsx index fe2f221..3ff6bfb 100644 --- a/frontend/app/subjects/[subjectId]/weeks/[weekNumber]/page.tsx +++ b/frontend/app/subjects/[subjectId]/weeks/[weekNumber]/page.tsx @@ -181,7 +181,7 @@ export default function WeekPage({ params }: { params: { subjectId: string; week

Semana {weekNumber}

{week?.title || "Sube fuentes y genera Markdown procesado."}

- {error && {error}} + {error && {error}}
@@ -298,7 +298,7 @@ export default function WeekPage({ params }: { params: { subjectId: string; week

Trabajos

- {jobs.map((job) =>
#{job.id}

{job.message}

)} + {jobs.map((job) =>
#{job.id}

{job.message}

)} {jobs.length === 0 &&

Sin trabajos.

}