No description
  • Python 85.2%
  • Jinja 6.6%
  • CSS 6.1%
  • Shell 1.6%
  • Dockerfile 0.5%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-08-26 00:41:01 -04:00
backend Add Apache 2.0 copyright headers to all code files 2026-08-26 00:13:40 -04:00
tests Add Apache 2.0 copyright headers to all code files 2026-08-26 00:13:40 -04:00
.env.template Initial commit: fabriquedoc engine without themes 2026-08-25 21:29:08 -04:00
.gitignore Initial commit: fabriquedoc engine without themes 2026-08-25 21:29:08 -04:00
AGENTS.md Initial commit: fabriquedoc engine without themes 2026-08-25 21:29:08 -04:00
deploy-ynh.sh Move Forgejo remote to partage/fabriquedoc-engine 2026-08-26 00:28:03 -04:00
docker-compose.yml Add Apache 2.0 copyright headers to all code files 2026-08-26 00:13:40 -04:00
generate_office_templates.py Add Apache 2.0 copyright headers to all code files 2026-08-26 00:13:40 -04:00
generate_test_docs.py Add Apache 2.0 copyright headers to all code files 2026-08-26 00:13:40 -04:00
generate_theme_variants.py Add Apache 2.0 copyright headers to all code files 2026-08-26 00:13:40 -04:00
LICENSE Initial commit: fabriquedoc engine without themes 2026-08-25 21:29:08 -04:00
poetry.lock Initial commit: fabriquedoc engine without themes 2026-08-25 21:29:08 -04:00
poetry.toml Initial commit: fabriquedoc engine without themes 2026-08-25 21:29:08 -04:00
pyproject.toml Initial commit: fabriquedoc engine without themes 2026-08-25 21:29:08 -04:00
pytest.ini Initial commit: fabriquedoc engine without themes 2026-08-25 21:29:08 -04:00
README.md docs: translate README to English 2026-08-26 00:41:01 -04:00

FabriqueDoc Engine

Renders Markdown into social-media images, presentations, animations, PDFs, and print documents — using SVG templates with themed branding.

Markdown → HTML → auto-pagination → SVG → (Playwright) → PNG/JPG/PDF
                                        → (ffmpeg)      → GIF/WebM/MP4
                                        → (python-pptx) → PPTX
                                        → (odfpy)       → ODP
                                        → (pandoc + theme template) → DOCX
                                        → (openpyxl + theme template) → XLSX
                                        → (LibreOffice headless) → ODT/ODS/ODP

Word (.docx) and Excel (.xlsx) exports are generated directly from Markdown via pandoc/openpyxl using a per-theme template (header, footer, colors, fonts) stored in backend/themes/<theme>/templates/. OpenDocument Spreadsheet (ODS) conversion is produced by LibreOffice headless from the XLSX file for identical rendering. Templates are regenerated by generate_office_templates.py.

Project Structure

fabriquedoc-engine/       ← this repository (engine only)
  backend/                ← FastAPI app: main.py:app (port 8080)
    core/                 ← renderer.py, svg_builder.py, html_builder.py, theme_manager.py
    models/               ← document.py (Pydantic DocumentSpecs)
    routers/              ← mcp.py, generer.py, images.py, format_styles.py
    themes/               ← _base/ (inherited defaults only)
    fonts/                ← Cinzel, FiraSans, etc.
  tests/                  ← pytest suite
fabriquedoc_ynh/          ← YunoHost package (separate git repo, nested)

Themes live in separate repositories. See:

Installation

# docker-compose.yml
services:
  app:
    build:
      context: ./backend
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
docker compose up --build

Application available at http://localhost:8080. Gradio UI at http://localhost:8080/ui/.

Environment variables — copy .env.template to .env:

FABRIQUEDOC_ENDPOINT=http://backend:8080

Option 2: Native Python

poetry install
uvicorn backend.main:app --host 0.0.0.0 --port 8080 --reload

Dockerfile

Container based on python:3.13-slim-bookworm with:

  • ffmpeg — video generation (GIF, WebM, MP4)
  • Playwright/Chromium — PDF, PNG, JPG, WebP rendering
  • 20 system dependencies required by Chromium

Key Commands

Command Description
uvicorn backend.main:app --reload Run locally
docker compose up --build Run via Docker
pytest Tests (-v --tb=short)
ruff check Lint (line-length=100, target=py311)
python generate_theme_variants.py --theme <name> Generate all format variants for a theme

Architecture

  • FastAPI: REST API at /generer/, /images/, /themes/, /formats/ + MCP at /mcp/
  • Gradio UI mounted at /ui/ via gr.mount_gradio_app
  • Render pipeline: Markdown → HTML → auto-paginate → SVG → (Playwright: PNG/JPG/WebP/PDF) → (ffmpeg: GIF/WebM/MP4) → (python-pptx/odfpy: PPTX/ODP) → (pandoc/openpyxl + theme templates: DOCX/XLSX) → (LibreOffice headless: ODS)
  • Theme system: _base/theme.yaml provides defaults; child themes override per field (inheritance in theme_manager.py)
  • Logo builder generates 3 variants (large/regular/2x) from icon.svg + product name via Jinja2
  • MCP endpoint: SSE transport at /mcp/sse

Formats

Defined in backend/formats.yaml. Each format has a category (social / presentation / print / custom) that automatically applies appropriate font sizes and title scales.

Format Dimensions Category
instagram-carre 2160×2160 social
instagram-story 2160×3840 social
instagram-fullscreen 2160×2700 social
facebook-post 2400×1256 social
linkedin-post 2400×1256 social
twitter-post 3200×1800 social
bluesky 2160×2160 social
pinterest 2000×3000 social
google-business 2400×1800 social
linkedin-banner 3168×792 social
slide169 1920×1080 presentation
slide43 1024×768 presentation
a4paper 2480×3507 (300 dpi) print
lettre 2550×3300 (300 dpi) print
custom 1200×800 custom

Social formats are 2× resolution for more content per page. Text is relatively smaller — use font_scale (DocumentSpecs or UI) to enlarge it. font_scale=2 restores the original appearance.

Themes

Themes live in backend/themes/. Each theme directory contains:

themes/<name>/
├── icon.svg              # Icon used for auto-generated logos
├── logo-large.svg        # Auto-generated logo (presentations, social)
├── logo-regular.svg      # Auto-generated logo (print formats)
├── logo-2x.svg           # Auto-generated logo (vertical social)
├── styles.css            # Optional CSS overrides
└── theme.yaml            # Theme configuration

SVG logos are auto-generated from icon.svg + product_name — no manual logo creation needed.

Built-in Themes

Theme Description
_base Defaults inherited by all themes. Dark background, gold accents.

Creating a Custom Theme

  1. Create themes/<name>/theme.yaml with base_theme: "_base":
name: "mytheme"
base_theme: "_base"
colors:
  title_color: "#e74c3c"
  text_color: "#2c3e50"
  background_color: "#ecf0f1"
layout:
  fontsize: 16
  margin: 80
  vmargin: 160

Only specified fields override the base theme — everything else is inherited.

  1. Add an icon.svg (any SVG, used for auto-generated logos).

  2. Optionally add a styles.css for custom CSS rules.

Parameters (DocumentSpecs)

Field Type Default Description
format string required Format ID
theme string required Theme ID
content string required Markdown content. ## creates new pages; \newpage forces a page break.
output_svg bool true SVG per page
output_pdf bool false Single PDF
output_png / output_jpg / output_webp bool false Raster per page
output_gif / output_webm / output_mp4 bool false Animated output
output_pptx / output_odp bool false Presentation output
output_md_slides bool false Markdown slides
output_zip bool false Bundle all outputs
fps int 15 Video frame rate
stilltime int 2 Seconds per slide in video
fontsize int null Base font size override (px)
font_scale float 1.0 Font size multiplier (e.g. 2 = double text size)
margin / vmargin int null Margin override (px)
width / height int null Custom dimensions for custom format
logo_variant string null "large", "regular", or "2x"

OAuth2 Authentication

FabriqueDoc uses OAuth 2.0 (RFC 6749) to secure MCP endpoints. REST endpoints (/images/, /generer/, /themes/, /formats/) also accept legacy API tokens (FABRIQUEDOC_API_TOKENS).

Configuration

Variable Description
FABRIQUEDOC_OAUTH_CLIENTS JSON {"client_id":"client_secret"} — pre-registered clients
FABRIQUEDOC_OAUTH_SECRET JWT signing key (auto-generated if empty, set in production)
FABRIQUEDOC_OAUTH_ALLOW_REGISTRATION true/false — allow dynamic registration
FABRIQUEDOC_API_TOKENS Legacy — simple API tokens (non-MCP)

Register a Client

Via web UI: https://fabriquedoc.service.jevalide.ca/oauth/register

Via API:

curl -X POST https://fabriquedoc.service.jevalide.ca/oauth/register \
  -H "Content-Type: application/json" \
  -d '{"client_name": "my-app"}'

Response:

{
  "client_id": "client_a1b2c3d4e5f6",
  "client_secret": "8dZIPD2O1OUFtH3SUV4FvWgkIOPCFjVk6k0d4u5KOv0",
  "client_id_issued_at": 1785016459,
  "client_secret_expires_at": 0,
  "redirect_uris": [],
  "client_name": "my-app",
  "grant_types": ["authorization_code", "client_credentials"],
  "response_types": ["code"]
}

Important: the client_secret is only shown at registration. Save it.

Obtain a Token

Via web UI: https://fabriquedoc.service.jevalide.ca/oauth/token

Via API (client credentials):

curl -X POST https://fabriquedoc.service.jevalide.ca/oauth/token \
  -d 'grant_type=client_credentials&client_id=client_a1b2c3d4e5f6&client_secret=8dZIPD2O1OUFtH3SUV4FvWgkIOPCFjVk6k0d4u5KOv0'

Response:

{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 31536000
}

Token is valid for 365 days (expires_in: 31536000 seconds).

Use the Token

Include the token in each MCP request:

curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
  https://fabriquedoc.service.jevalide.ca/mcp/capabilities

For SSE transport, the token can also be passed as a query parameter:

https://fabriquedoc.service.jevalide.ca/mcp/sse?token=eyJhbGciOiJIUzI1NiIs...

Rotate a Secret

Invalidates the current secret and generates a new one. The client_id remains the same.

Via web UI: https://fabriquedoc.service.jevalide.ca/oauth/token (section "Rotate secret")

Via API:

curl -X POST https://fabriquedoc.service.jevalide.ca/oauth/rotate \
  -d 'client_id=client_a1b2c3d4e5f6&client_secret=OLD_SECRET'

Response (new secret + new token):

{
  "client_id": "client_a1b2c3d4e5f6",
  "client_secret": "NEW_SECRET",
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "token_type": "Bearer",
  "expires_in": 31536000
}

OAuth2 Endpoints

Endpoint Method Description
/.well-known/oauth-authorization-server GET OAuth2 metadata (RFC 8414)
/oauth/ GET Dashboard
/oauth/register GET/POST Register a client
/oauth/token GET/POST Obtain a token
/oauth/rotate POST Rotate a client secret
/oauth/authorize GET Authorization endpoint (browser flow)

Since v2.3, FabriqueDoc provides a durable authentication mechanism replacing the OAuth2 flow: accounts with email verification, and long-lived API keys accepted on MCP endpoints.

  1. Create an account: /accounts/register (email + password).
  2. Confirm your email via the link sent by SMTP (24h, single use).
  3. Log in: /accounts/login, then create an API key on /accounts/keys.
  4. A key is valid until revoked, or expires after one year ("never expire" option available at creation).

Keys are stored hashed (SHA-256) and can be revoked at any time from /accounts/keys. The UI displays the JSON block to paste into your MCP configuration:

{
  "mcpServers": {
    "fabriquedoc": {
      "type": "remote",
      "url": "https://fabriquedoc.service.jevalide.ca/mcp",
      "headers": {
        "Authorization": "Bearer fd_..."
      }
    }
  }
}

Environment variable configuration:

Variable Description
FABRIQUEDOC_DB SQLite file (accounts + API keys). Default: fabriquedoc.db
FABRIQUEDOC_MAIL_FROM Verification email sender
FABRIQUEDOC_SMTP_HOST / FABRIQUEDOC_SMTP_PORT SMTP server (default localhost:25)
FABRIQUEDOC_SMTP_USER / FABRIQUEDOC_SMTP_PASS Optional SMTP auth (STARTTLS)
FABRIQUEDOC_SESSION_DAYS Web session duration (default 30 days)
FABRIQUEDOC_KEY_DEFAULT_DAYS API key lifetime (default 365 days)

MCP Endpoints

  • GET /mcp/health — liveness (no auth)
  • GET /mcp/capabilities — discover themes, formats, schema
  • POST /mcp — Streamable HTTP transport (JSON-RPC)
  • GET /mcp/sse — SSE transport (JSON-RPC)
  • POST /mcp/messages — SSE messages

Examples

Simple SVG:

curl -X POST http://localhost:8080/mcp/invoke \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "action": "generate",
    "specs": {
      "format": "instagram-carre",
      "theme": "_base",
      "content": "## Hello\n\nThis is a test."
    }
  }' \
  --output result.zip

PDF + PPTX:

curl -X POST http://localhost:8080/mcp/invoke \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "action": "generate",
    "specs": {
      "format": "slide169",
      "theme": "_base",
      "content": "## Slide 1\n\nContent\n\n## Slide 2\n\nMore content",
      "output_pdf": true,
      "output_pptx": true
    }
  }' \
  --output result.zip

Custom dimensions:

curl -X POST http://localhost:8080/mcp/invoke \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "action": "generate",
    "specs": {
      "format": "custom",
      "theme": "_base",
      "width": 728,
      "height": 90,
      "content": "## Banner text",
      "output_png": true
    }
  }' \
  --output result.zip

MCP Client Configuration

FabriqueDoc exposes two MCP transports:

  • Streamable HTTP (recommended for opencode): POST /mcp
  • SSE (Claude Desktop): GET /mcp/sse

Both transports require a valid OAuth2 token.

opencode — Automatic OAuth2

opencode detects the 401 and initiates the OAuth2 flow automatically:

{
  "mcp": {
    "fabriquedoc": {
      "type": "remote",
      "url": "https://fabriquedoc.service.jevalide.ca/mcp",
      "oauth": {}
    }
  }
}

Launch authentication:

opencode mcp auth fabriquedoc

opencode — Pre-registered Client

{
  "mcp": {
    "fabriquedoc": {
      "type": "remote",
      "url": "https://fabriquedoc.service.jevalide.ca/mcp",
      "oauth": {
        "clientId": "client_a1b2c3d4e5f6",
        "clientSecret": "8dZIPD2O1OUFtH3SUV4FvWgkIOPCFjVk6k0d4u5KOv0"
      }
    }
  }
}

opencode — Static Token

{
  "mcp": {
    "fabriquedoc": {
      "type": "remote",
      "url": "https://fabriquedoc.service.jevalide.ca/mcp",
      "headers": {
        "Authorization": "Bearer {env:FABRIQUEDOC_TOKEN}"
      }
    }
  }
}

Claude Desktop

{
  "mcpServers": {
    "fabriquedoc": {
      "type": "url",
      "url": "https://fabriquedoc.service.jevalide.ca/mcp/sse",
      "headers": {
        "Authorization": "Bearer YOUR_TOKEN"
      }
    }
  }
}

Markdown Features

  • ## Heading — creates a new page/slide
  • \newpage — manual page break
  • ![alt](/images/filename.png) — embed images
  • **bold**, *italic*, `code`, ~~strikethrough~~
  • Code blocks with syntax highlighting
  • Blockquotes, lists, tables, links
  • ::: {.center} — custom CSS class wrapper

Image Management

Images go in themes/_base/resources/. Reference them as ![alt](/images/filename.png). Manage via the Gradio UI (/ui/) or POST /images/.

Packaging for YunoHost

YunoHost is a server operating system that simplifies self-hosting. Once packaged, an application benefits from:

  • Automatic nginx reverse-proxy with Let's Encrypt SSL
  • Managed systemd service (start, stop, restart)
  • Built-in backup and restore
  • Updates via yunohost app upgrade
  • Administration portal for configuration

Package Structure

fabriquedoc_ynh/
├── manifest.json          # Metadata + declarative resources
├── scripts/
│   ├── install
│   ├── upgrade
│   ├── remove
│   ├── backup
│   ├── restore
│   └── change_url
├── conf/
│   ├── nginx.conf          # nginx template (__PATH__, __PORT__)
│   ├── systemd.service     # systemd template (__APP__, __INSTALL_DIR__, etc.)
│   └── config_panel.toml   # Configuration panel in YNH portal
├── app/                    # Backend source copy (rsync from parent repo)
├── build.sh
└── CHANGELOG.md

Manifest

{
  "packaging_format": 2,
  "id": "fabriquedoc",
  "version": "2.0.0~ynh9",
  "resources": {
    "system_user": { "main": {} },
    "ports": { "main": { "default": 8080, "service": "fabriquedoc" } },
    "install_dir": {},
    "permissions": {
      "main": { "allowed": "visitors", "auth_header": false, "url": "/" }
    },
    "apt": {
      "packages": [
        "ffmpeg", "python3", "python3-venv", "python3-pip",
        "libnss3", "libnspr4", "libatk1.0-0", "libcups2",
        "libdrm2", "libgbm1", "libpango-1.0-0", "libcairo2",
        "libasound2", "libxkbcommon0", "libxcomposite1",
        "libxdamage1", "libxfixes3", "libxrandr2",
        "libatk-bridge2.0-0", "libdbus-1-3", "libatspi2.0-0"
      ]
    }
  },
  "install": {
    "domain": { "type": "domain", "ask": { "en": "Choose a domain", "fr": "Choisissez un domaine" } },
    "path": { "type": "path", "default": "/" },
    "images_dir": {
      "type": "string",
      "ask": { "en": "Directory for uploaded images", "fr": "Dossier pour les images" },
      "default": "/var/www/fabriquedoc/images"
    }
  }
}

Installation Script

# 1. Copy sources
cp -a "$YNH_APP_BASEDIR/app/." "$install_dir/app/"

# 2. Create venv
python3 -m venv "$install_dir/app/.venv"
"$install_dir/app/.venv/bin/pip" install --upgrade pip setuptools wheel
"$install_dir/app/.venv/bin/pip" install -r "$install_dir/app/requirements.txt"

# 3. Install Chromium for Playwright
PLAYWRIGHT_BROWSERS_PATH=/var/cache/ms-playwright \
  "$install_dir/app/.venv/bin/playwright" install chromium

# 4. Create images directory (with FUSE guard)
mkdir -p "$images_dir" 2>/dev/null || true
chown -R "$app:$app" "$images_dir" 2>/dev/null || true

# 5. Install config panel
cp "$install_dir/app/conf/config_panel.toml" "/etc/yunohost/apps/$app/config_panel.toml"
sed -i "s/__APP__/$app/g; s|__IMAGES_DIR__|$images_dir|g" "/etc/yunohost/apps/$app/config_panel.toml"

# 6. Configure nginx and systemd
ynh_add_nginx_config
ynh_add_systemd_config

# 7. Register service
yunohost service add "$app"

Deployment and Updates

# Build package
rsync -a --exclude='__pycache__' --exclude='.venv' backend/ fabriquedoc_ynh/app/
cd fabriquedoc_ynh
./build.sh     # → ../fabriquedoc_ynh_2.0.0.tar.gz

# Install on server
yunohost app install ../fabriquedoc_ynh_2.0.0.tar.gz

# Update
rsync -a --exclude='__pycache__' --exclude='.venv' backend/ fabriquedoc_ynh/app/
# Bump version in manifest.json
# Commit both repos
./build.sh
scp fabriquedoc_ynh_2.0.0.tar.gz server:/tmp/
ssh server "yunohost app upgrade fabriquedoc -f /tmp/fabriquedoc_ynh_2.0.0.tar.gz"

Lessons Learned

Iteration Fix
ynh1 → ynh2 Helpers v2, __INSTALL_DIR__, package names without t64, simplified nginx
ynh2 → ynh3 Public access, configurable images directory
ynh3 → ynh4 TOML config panel
ynh4 → ynh5 service key on ports resource
ynh5 → ynh6 yunohost service add/remove, FUSE guards
ynh6 → ynh7 Config panel persistence via app/conf/
ynh7 → ynh8 Backend sync, MCP protocol fix
ynh8 → ynh9 output_root in MCP path, async rendering

YNH Packaging Notes

  • Helpers v2 vs v2.1 — YNH 11.x uses v2 helpers. Function names differ: ynh_add_nginx_config (v2) vs ynh_config_add_nginx (v2.1, non-existent).
  • Injected variables$install_dir, $domain, $path, $port are auto-injected by YNH into script environments.
  • Hardcoded template namesnginx.conf for nginx, systemd.service for systemd.
  • Single location block in nginx.conf — YNH wraps it with the server, listen, and SSL.
  • Config panel — must be copied from app/conf/ to survive updates.
  • Register serviceyunohost service add is required even with a valid systemd file.
  • FUSE mounts — protect mkdir and chown with || true.
  • Debian 12 — does not have t64 packages.

Operational Notes

  • Remote MCP: opencode connects via "type": "remote" to https://fabriquedoc.service.jevalide.ca/mcp/sse
  • Environment variables: FABRIQUEDOC_ENDPOINT, FABRIQUEDOC_PUBLIC_ENDPOINT (see .env.template), FABRIQUEDOC_IMAGES_DIR
  • No CI/CD, no pre-commit hooks

Tests

The test suite starts the real application (uvicorn) in a subprocess with a throwaway SQLite database, isolated images directory, and unreachable SMTP — verification links are read from the server log instead of being sent by email.

.venv/bin/python -m playwright install chromium   # once
.venv/bin/python -m pytest                        # 6 tests: UI (Playwright) + API + MCP
  • tests/test_ui_accounts.py — full browser flow: register → email verification → login → create API key → revoke.
  • tests/test_api_generate.py — REST /generer/ with an API key.
  • tests/test_mcp_generate.py — JSON-RPC via /mcp (Streamable HTTP) with an API key, then file download.

License

Apache License 2.0 — see LICENSE for details.