- Python 85.2%
- Jinja 6.6%
- CSS 6.1%
- Shell 1.6%
- Dockerfile 0.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| backend | ||
| tests | ||
| .env.template | ||
| .gitignore | ||
| AGENTS.md | ||
| deploy-ynh.sh | ||
| docker-compose.yml | ||
| generate_office_templates.py | ||
| generate_test_docs.py | ||
| generate_theme_variants.py | ||
| LICENSE | ||
| poetry.lock | ||
| poetry.toml | ||
| pyproject.toml | ||
| pytest.ini | ||
| README.md | ||
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:
- fabriquedoc-themes — all themes
- fabriquedoc-theme-jevalideca — jevalideca variants
- fabriquedoc-theme-clinique-mobile
- fabriquedoc-theme-medint
Installation
Option 1: Docker Compose (recommended)
# 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/viagr.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.yamlprovides defaults; child themes override per field (inheritance intheme_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) | |
lettre |
2550×3300 (300 dpi) | |
custom |
1200×800 | custom |
Social formats are 2× resolution for more content per page. Text is relatively smaller — use
font_scale(DocumentSpecsor UI) to enlarge it.font_scale=2restores 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
- Create
themes/<name>/theme.yamlwithbase_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.
-
Add an
icon.svg(any SVG, used for auto-generated logos). -
Optionally add a
styles.cssfor 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_secretis 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) |
Accounts & API Keys (recommended)
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.
- Create an account:
/accounts/register(email + password). - Confirm your email via the link sent by SMTP (24h, single use).
- Log in:
/accounts/login, then create an API key on/accounts/keys. - 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, schemaPOST /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— 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 . 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) vsynh_config_add_nginx(v2.1, non-existent). - Injected variables —
$install_dir,$domain,$path,$portare auto-injected by YNH into script environments. - Hardcoded template names —
nginx.conffor nginx,systemd.servicefor 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 service —
yunohost service addis required even with a valid systemd file. - FUSE mounts — protect
mkdirandchownwith|| true. - Debian 12 — does not have
t64packages.
Operational Notes
- Remote MCP: opencode connects via
"type": "remote"tohttps://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.