diff --git a/.env.example b/.env.example index 8409717..7b27d6f 100644 --- a/.env.example +++ b/.env.example @@ -71,6 +71,16 @@ OPENAI_HEALTH_CHECK_TIMEOUT=5 OPENROUTER_API_KEY= OPENROUTER_MODEL=deepseek/deepseek-chat +# Mistral OCR — translation of SCANNED PDFs (image-only pages). +# Optional. If absent: scanned PDFs are rejected with an explicit error. +# Get API key from: https://console.mistral.ai/ (~$1 / 1000 pages) +MISTRAL_API_KEY= +MISTRAL_OCR_MODEL=mistral-ocr-latest +MISTRAL_OCR_TIMEOUT=180 +MISTRAL_OCR_ENABLED=true +# Average extractable chars/page below which a PDF is treated as scanned +SCANNED_PDF_MIN_CHARS_PER_PAGE=100 + # Ollama Configuration. Optional. If absent: default http://localhost:11434 (provider may be disabled). OLLAMA_BASE_URL=http://localhost:11434 OLLAMA_MODEL=llama3 diff --git a/.env.ionos b/.env.ionos deleted file mode 100644 index d736f5e..0000000 --- a/.env.ionos +++ /dev/null @@ -1,93 +0,0 @@ -# ============================================================ -# PRODUCTION — Ionos VPS -# Copiez ce fichier en .env sur le serveur et remplissez TOUT -# Ne committez JAMAIS ce fichier avec de vraies valeurs -# ============================================================ - -# ─── Application ──────────────────────────────────────────── -APP_NAME="Office Translator" -ENV=production -DEBUG=false -LOG_LEVEL=INFO -LOG_FORMAT=json - -# ─── Domaine & URLs ───────────────────────────────────────── -# Remplacez par votre vrai domaine (configuré dans Ionos DNS) -DOMAIN=wordly.art -NEXT_PUBLIC_API_URL=https://wordly.art -CORS_ORIGINS=https://wordly.art - -# ─── Sécurité JWT ───────────────────────────────────────── -# OBLIGATOIRE — Générez avec : -# python3 -c "import secrets; print(secrets.token_urlsafe(64))" -JWT_SECRET_KEY=REMPLACEZ_PAR_UNE_CLE_SECRETE_DE_64_CARACTERES - -# ─── Admin ────────────────────────────────────────────────── -ADMIN_USERNAME=admin -# Hash bcrypt de votre mot de passe admin -# Générez avec : python3 -c "from passlib.context import CryptContext; print(CryptContext(schemes=['bcrypt']).hash('VotreMotDePasse'))" -ADMIN_PASSWORD_HASH=REMPLACEZ_PAR_HASH_BCRYPT - -# ─── Base de données PostgreSQL ───────────────────────────── -POSTGRES_USER=translate -# Mot de passe fort — minimum 32 caractères -# Générez avec : python3 -c "import secrets; print(secrets.token_urlsafe(32))" -POSTGRES_PASSWORD=REMPLACEZ_PAR_MOT_DE_PASSE_FORT -POSTGRES_DB=translate_db -DATABASE_URL=postgresql+asyncpg://translate:REMPLACEZ_PAR_MOT_DE_PASSE_FORT@postgres:5432/translate_db - -# ─── Redis ────────────────────────────────────────────────── -REDIS_URL=redis://redis:6379/0 - -# ─── Fournisseurs de traduction ───────────────────────────── -# Google Translate (gratuit, via deep_translator) — toujours activé -GOOGLE_TRANSLATE_ENABLED=true - -# Google Cloud Translation API (payant) -GOOGLE_CLOUD_ENABLED=false -GOOGLE_CLOUD_API_KEY= - -# DeepL -DEEPL_ENABLED=false -DEEPL_API_KEY= - -# OpenRouter (IA) -OPENROUTER_ENABLED=false -OPENROUTER_API_KEY= -OPENROUTER_MODEL=deepseek/deepseek-v3.2 - -# OpenAI -OPENAI_ENABLED=false -OPENAI_API_KEY= -OPENAI_MODEL=gpt-4o-mini - -# Désactivés en production standard -OLLAMA_ENABLED=false -ZAI_API_KEY= - -# ─── Stripe Paiements ─────────────────────────────────────── -STRIPE_SECRET_KEY=sk_live_REMPLACEZ -STRIPE_WEBHOOK_SECRET=whsec_REMPLACEZ -STRIPE_PRICE_STARTER_MONTHLY=price_REMPLACEZ -STRIPE_PRICE_STARTER_YEARLY=price_REMPLACEZ -STRIPE_PRICE_PRO_MONTHLY=price_REMPLACEZ -STRIPE_PRICE_PRO_YEARLY=price_REMPLACEZ -STRIPE_PRICE_BUSINESS_MONTHLY=price_REMPLACEZ -STRIPE_PRICE_BUSINESS_YEARLY=price_REMPLACEZ - -# ─── Fichiers & Limites ───────────────────────────────────── -MAX_FILE_SIZE_MB=50 -MAX_CONCURRENT_TRANSLATIONS=5 -CLEANUP_ENABLED=true -FILE_TTL_MINUTES=60 - -# ─── HTTPS / HSTS ─────────────────────────────────────────── -ENABLE_HSTS=true -LETSENCRYPT_EMAIL=votre-email@wordly.art - -# ─── Rate limiting ────────────────────────────────────────── -RATE_LIMIT_ENABLED=true -RATE_LIMIT_PER_MINUTE=30 -RATE_LIMIT_PER_HOUR=200 -TRANSLATIONS_PER_MINUTE=5 -TRANSLATIONS_PER_HOUR=30 diff --git a/.env.production b/.env.production deleted file mode 100644 index 9ddedd1..0000000 --- a/.env.production +++ /dev/null @@ -1,106 +0,0 @@ -# ============================================ -# Wordly.art - .env de production -# ============================================ -# Copier ce fichier en .env sur le serveur -# cp .env.production .env -# Puis remplacer les valeurs [A CHANGER] -# ============================================ - -# ---- Application ---- -APP_NAME=Wordly -APP_ENV=production -DEBUG=false -LOG_LEVEL=INFO - -# ---- Domaine ---- -DOMAIN=wordly.art -NEXT_PUBLIC_API_URL=https://wordly.art -FRONTEND_URL=https://wordly.art -BACKEND_PORT=8000 -FRONTEND_PORT=3000 - -# ---- Service de traduction par defaut ---- -# Choisir: google, ollama, deepseek, minimax, deepl, openai, openrouter -TRANSLATION_SERVICE=google - -# ---- Google (gratuit, toujours actif) ---- -GOOGLE_TRANSLATE_ENABLED=true - -# ---- Google OAuth (connexion avec Google) ---- -GOOGLE_CLIENT_ID= -NEXT_PUBLIC_GOOGLE_CLIENT_ID= - -# ---- Ollama (local, gratuit) ---- -OLLAMA_ENABLED=false -OLLAMA_BASE_URL=http://ollama:11434 -OLLAMA_MODEL=llama3 - -# ---- DeepSeek ---- -DEEPSEEK_ENABLED=false -DEEPSEEK_API_KEY= -DEEPSEEK_MODEL=deepseek-chat -DEEPSEEK_BASE_URL=https://api.deepseek.com/v1 - -# ---- Minimax ---- -MINIMAX_ENABLED=false -MINIMAX_API_KEY= -MINIMAX_MODEL=MiniMax-M1 -MINIMAX_BASE_URL=https://api.minimax.chat/v1 - -# ---- DeepL ---- -DEEPL_ENABLED=false -DEEPL_API_KEY= - -# ---- OpenAI ---- -OPENAI_ENABLED=false -OPENAI_API_KEY= -OPENAI_MODEL=gpt-4o-mini -OPENAI_BASE_URL=https://api.openai.com/v1 - -# ---- OpenRouter ---- -OPENROUTER_ENABLED=false -OPENROUTER_API_KEY= -OPENROUTER_MODEL=deepseek/deepseek-chat - -# ---- Upload ---- -MAX_FILE_SIZE_MB=50 -ALLOWED_EXTENSIONS=.docx,.xlsx,.pptx - -# ---- Rate Limiting ---- -RATE_LIMIT_ENABLED=true -RATE_LIMIT_REQUESTS_PER_MINUTE=60 -RATE_LIMIT_TRANSLATIONS_PER_MINUTE=10 -RATE_LIMIT_TRANSLATIONS_PER_HOUR=100 -RATE_LIMIT_TRANSLATIONS_PER_DAY=500 - -# ---- Admin ---- -ADMIN_USERNAME=admin -ADMIN_PASSWORD_HASH=CHANGE_WITH_BCRYPT_HASH - -# ---- Secrets (deja generes) ---- -JWT_SECRET_KEY=84-MniOv3rOZZ3FczwDvsNNHqZRf8yKI06uMNQGAgMSV8yAJ19comNe6FHHcnVyeNm-fvDxlcb9CWe40y4oy7A -ADMIN_TOKEN_SECRET=1301880be1bc8026676d7f4fb13ae1c70fcd2abbd2f2cdfcb044eea5c7005ce3 -CORS_ORIGINS=https://wordly.art - -# ---- Database ---- -POSTGRES_USER=translate -POSTGRES_PASSWORD=yLLgkEvt6mvzGDdoqtQvI1vEgMmR-W75ZTPW5StaIAU -POSTGRES_DB=translate_db - -# ---- Monitoring ---- -GRAFANA_USER=admin -GRAFANA_PASSWORD=WordlyGrafana2026! - -# ---- Stripe ---- -# Clés TEST — remplacer par sk_live_... / pk_live_... en production réelle -STRIPE_PUBLISHABLE_KEY=pk_test_51SkSHkCKXUJE51jnCr2QV4vCE18GH1XF59eHrHOV46EORuZTPVXFXxrbcJamyoJLaUHMc2McCRVkU4b6VMAWVR2R00XRdwmXMx -STRIPE_SECRET_KEY=sk_test_51SkSHkCKXUJE51jnbEtXZ0nKiTHTa8ohDwLH8fZiDVEx6Ze0g5dg4fGJJgX1VgNHvF93GE3HTramT3oQrCaqOxid00OXTcZlsW -STRIPE_WEBHOOK_SECRET= - -# Price IDs créés dans Stripe Dashboard (wordly.art — test mode) -STRIPE_PRICE_STARTER_MONTHLY=price_1TdF8FCKXUJE51jnNAeLqhF3 -STRIPE_PRICE_STARTER_YEARLY=price_1TdF8GCKXUJE51jnBhVVrjrh -STRIPE_PRICE_PRO_MONTHLY=price_1TdF8GCKXUJE51jn9ChAAhKM -STRIPE_PRICE_PRO_YEARLY=price_1TdF8HCKXUJE51jnpsvBivDe -STRIPE_PRICE_BUSINESS_MONTHLY=price_1TdF8HCKXUJE51jn2K9EeBGJ -STRIPE_PRICE_BUSINESS_YEARLY=price_1TdF8ICKXUJE51jnmgWZxW4U diff --git a/.gitignore b/.gitignore index 514f5c2..4f1e7ab 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,7 @@ backups/ # IDE .vscode/ .idea/ +.zcode/ *.swp *.swo @@ -46,11 +47,18 @@ outputs/ temp/ translated_files/ translated_test.* +/translations.db + +# Generated translation test outputs in the sample corpus (regeneratable, not for commit) +# (the tracked sample_files/test_corpus/test_pdf_translated.pdf stays tracked) +sample_files/test_corpus/*_translated* +sample_files/test_corpus/*rigoureuse* # Runtime data (users, provider config, glossaries) — managed at runtime, not in git data/users.json data/*.db data/*.sqlite +data/waitlist.json # Keep these in git (templates/defaults only) # data/provider_settings.json → commiter uniquement si pas de clés dedans @@ -88,4 +96,14 @@ htmlcov/ # Auto-generated pnpm workspace file (placeholders) frontend/pnpm-workspace.yaml +office-translator-landing-page/pnpm-workspace.yaml + +# Secrets et donnees locales (audit securite 2026-08-26) +data/provider_settings.json + +# Unrelated local project (separate repo, never commit here) +Colonization/ + +# TypeScript incremental build artifact +office-translator-landing-page/tsconfig.tsbuildinfo diff --git a/MARKETING_PLAN.md b/MARKETING_PLAN.md index 7f218f9..d484df9 100644 --- a/MARKETING_PLAN.md +++ b/MARKETING_PLAN.md @@ -1,211 +1,208 @@ -# Plan Marketing - Office Translator (SaaS de Traduction de Documents) +# Plan Marketing — Office Translator (Wordly.art) -> Document de référence pour l'agent marketing. Dernière mise à jour : 2026-05-10 +> Document de référence pour l'agent marketing. Dernière mise à jour : 2026-08-29 +> +> **Changements majeurs vs. version du 2026-05-10** +> 1. **Le billing est réellement implémenté** (Stripe : checkout, webhooks, portail, crédits) — la section tarifaire est désormais ancrée sur les plans réels du backend (`models/subscription.py`, `services/pricing_config.py`), plus sur des tiers fictifs. +> 2. **La landing page est fonctionnelle** : sections pricing, FAQ, preuve sociale, capture d'e-mail (waitlist) et analytics (Vercel Analytics) sont déployées dans `office-translator-landing-page/`. +> 3. **Un endpoint public d'inscription** existe : `POST /api/v1/waitlist` (dédoublonné, persisté dans `data/waitlist.json`), compteur `GET /api/v1/waitlist/count`. +> 4. La mention « SOC 2 Compliant » (infondée) a été retirée du footer. +> +> **Alignement code du 2026-08-29** (voir `docs/marketing/PLAN-ALIGNEMENT-CODE.md`) : le PDF est supporté (dont **PDF scannés via OCR Mistral**) et l'ajoutera à tous les supports ; rétention réelle = envois 30 min / résultats ≤ 2 h ; les moteurs vendus sont exactement ceux des plans (`PLANS[plan]["providers"]`, désormais appliqué côté API) ; 100+ langues exposées via `/api/v1/languages`. --- ## 1. Positionnement & Proposition de Valeur ### Le Produit -**Office Translator** est un service SaaS de traduction de documents professionnels (Word, Excel, PowerPoint) qui **préserve parfaitement la mise en forme, les tableaux, les images et les styles** du document original. +**Office Translator** (marque **Wordly.art**) est un SaaS de traduction de documents professionnels (Word, Excel, PowerPoint, **PDF — y compris scannés, via OCR**) qui **préserve la mise en forme, les formules, les tableaux, les images et les styles** du document original. L'utilisateur choisit son moteur de traduction par document. -### Ce qui nous différencie -- **Préservation du format** : Contrairement à Google Translate ou DeepL qui détruisent les layouts complexes, notre moteur maintient la structure exacte -- **Multi-providers** : L'utilisateur choisit son moteur (Google, DeepL, OpenAI, DeepSeek, OpenRouter, Minimax, Zai) selon son budget et ses besoins -- **Glossaires techniques** : Terminologie personnalisée (HVAC, IT, Juridique, Médical) +> Nommage : « Office Translator » est le nom du dépôt / nom de travail interne ; **Wordly.art** est la marque publique (domaine de lancement, landing, footer). Le footer de la landing affiche donc « Wordly.art » — c'est intentionnel. -### Marché cible -| Segment | Taille estimée | Priorité | -|---------|---------------|----------| -| PME internationales ( traductions régulières) | Grand | P1 | -| Agences de traduction (productivité) | Moyen | P1 | -| Départements RH multilingues | Moyen | P2 | -| Freelancers / consultants | Grand | P2 | -| Étudiants & academics | Grand | P3 (freemium) | +### Accroche unique (USP) +> « Traduit en place. Zéro perte de mise en page. » + +Contrairement à Google Translate (qui aplatit le fichier en texte brut) ou à DeepL (formatage limité), le moteur translate *in place* : cellules fusionnées, formules Excel, en-têtes/pieds de page Word, animations PowerPoint restent intacts. Les PDF scannés (pages image) sont récupérés par **OCR Mistral** avant traduction — DeepL et Azure les refusent. Les textes dans les images sont traduits via des modèles vision (plans payants). + +### Différenciateurs +| Différenciateur | Détail | Preuve produit | +|---|---|---| +| Préservation du format | Structure préservée (formules, fusions, styles) ; PDF scannés via OCR | openpyxl / python-docx / python-pptx / PyMuPDF + `services/mistral_ocr.py`, tests de réintégration | +| Multi-moteurs (7) | Google, DeepL, Google Cloud, OpenRouter éco **et** premium, OpenAI, Grok (xAI) — grille appliquée côté API par plan | Choix par document, `PLANS[plan]["providers"]` | +| IA contextuelle | Traduction LLM contextuelle (DeepSeek / Claude / Gemini) sur plans payants | `ai_translation` + `ai_tier` | +| Glossaires techniques | Terminologie personnalisée (CVC, IT, Légal, Médical…) | `routes/glossary_routes.py` | +| 60+ langues | 100+ codes validés, exposés via `/api/v1/languages` | `middleware/validation.py` | +| Confiance | Suppression auto des fichiers (envois 30 min, résultats ≤ 2 h), TLS, jamais utilisé pour l'entraînement | `config.py` (TTLs), docs sécurité | + +### Segments cibles (priorisés) +| Segment | Douleur | Taille | Priorité | Message clé | +|---|---|---|---|---| +| **PME internationales** (export, docs commerciaux/techniques récurrents) | Traducteurs humains chers et lents, formats cassés | Grand | **P1** | « Le coût par page, sans casser le format » | +| **Agences de traduction** (productivité) | Revue manuelle des mises en page, marges écrasées | Moyen | **P1** | « 7 moteurs au choix, glossaires partagés, 5 sièges » | +| **Départements RH multilingues** (contrats, onboarding) | Volume saisonnier, terminologie juridique | Moyen | P2 | « Glossaires légaux + volume Business » | +| **Freelances / consultants** | Outils gratuits qui détruisent leur travail | Grand | P2 | « 2 docs gratuits/mois, Starter à 9 € » | +| **Étudiants & académiques** | Budget nul | Grand | P3 (freemium) | « Plan Free, sans carte bancaire » | --- -## 2. Supports Visuels Requis +## 2. Tarification (source de vérité : `models/subscription.py` + `services/pricing_config.py`) -### A. Captures d'écran (OBLIGATOIRES - priorité maximale) +**Monnaie : EUR. Billing Stripe implémenté (checkout, webhooks, portail client, crédits à l'unité).** -| # | Capture | Usage | Instructions | -|---|---------|-------|-------------| -| 1 | **Page d'accueil / Hero** | Landing page, réseaux sociaux | Montrer l'interface épurée avec le drop-zone de fichier | -| 2 | **Upload en cours** | Démonstration du workflow | Fichier Excel chargé avec sélection langue source/cible | -| 3 | **Résultat côte à côte** | Preuve de qualité | Document original vs traduit, montrer que le format est intact | -| 4 | **Sélection du provider** | Fonctionnalité clé | Dropdown Google/DeepL/OpenAI/DeepSeek avec prix affichés | -| 5 | **Glossaire technique** | Différenciation | Interface de gestion des glossaires personnalisés | -| 6 | **Dashboard admin** | Crédibilité entreprise | Vue monitoring avec statistiques d'utilisation | -| 7 | **Page pricing/forfaits** | Conversion | Les 3 tiers (Starter/Pro/Business) clairement affichés | -| 8 | **Profil utilisateur** | Confiance | Page de profil avec historique et quota | +| Plan | Mensuel | Annuel (−20 %) | Docs/mois | Pages/doc max | Fichier max | Moteurs | IA | API | +|---|---|---|---|---|---|---|---|---| +| **Free** | 0 € | — | 2 | 10 | 5 Mo | Google | Non (filigrane) | Non | +| **Starter** | 9 € | 7,20 €/mois | 50 | 50 | 10 Mo | Google, DeepL | Non | Non | +| **Pro** ★ populaire | 19 € | 15,20 €/mois | 200 | 200 | 25 Mo | + Google Cloud, OpenRouter | Essentielle (DeepSeek) | Non | +| **Business** | 49 € | 39,20 €/mois | 1 000 | 500 | 50 Mo | + OpenAI, x.ai/Zai, premium | Premium (Claude/Gemini) | Oui, 10 000 appels/mois, 5 sièges | +| **Enterprise** | sur demande | sur demande | illimité | illimité | illimité | + custom | custom | illimitable | -**Format** : PNG, 1280x720 minimum, fond clair et sombre - -### B. Vidéo de Démonstration (OBLIGATOIRE) - -#### Vidéo courte (60-90 secondes) - "How it works" -- **Objectif** : Landing page + réseaux sociaux -- **Script suggéré** : - 1. (0-5s) Logo + tagline animé - 2. (5-15s) Problème : "Traduire un Excel de 50 pages sans casser le format ? Mission impossible." - 3. (15-40s) Démo accélérée : Upload → Sélection langue → Provider → Traduction → Download - 4. (40-55s) Split-screen : document original vs traduit, zoom sur tableaux/images intacts - 5. (55-65s) CTA : "Essayez gratuitement" + URL -- **Format** : MP4 1080p, sous-titres FR + EN - -#### Vidéo tutoriel (3-5 minutes) - "Guide complet" -- **Objectif** : YouTube, onboarding utilisateurs -- **Contenu** : Création de compte, upload, glossaires, providers, download -- **Format** : Screencast avec voiceover - -### C. Autres assets visuels - -| Asset | Spécifications | -|-------|---------------| -| **Logo SVG** | Version claire + sombre, icône seule + avec texte | -| **OG Image** | 1200x630px pour partage réseaux sociaux | -| **Favicon** | 32x32, 16x16, ICO + PNG | -| **Bannière GitHub** | 1280x640px pour le repo README | -| **GIF animé** | 15s loop du workflow upload→traduction→download | -| **Infographie** | "Pourquoi Office Translator" - comparaison avant/après | - ---- - -## 3. Canaux de Lancement - -### Phase 1 : Pré-lancement (Semaines 1-2) - -| Canal | Action | Priorité | -|-------|--------|----------| -| **Landing page** | Mettre en place avec captures d'écran + vidéo + formulaire email | CRITIQUE | -| **Product Hunt** | Préparer le launch (assets, description, maker comment) | HAUTE | -| **Reddit** | Posts dans r/SideProject, r/saas, r/translator | HAUTE | -| **Hacker News** | Préparer un "Show HN" technique | HAUTE | -| **Twitter/X** | Thread de lancement avec démo GIF | MOYENNE | -| **LinkedIn** | Post professionnel ciblant PME et agences | MOYENNE | - -### Phase 2 : Lancement (Semaine 3) - -| Canal | Action | -|-------|--------| -| **Product Hunt** | Lancement le mardi ou mercredi (meilleur trafic) | -| **Reddit** | Cross-post dans 5-6 subreddits pertinents | -| **Hacker News** | Soumettre le Show HN le matin (heure EST) | -| **Twitter/X** | Thread avec GIF + link Product Hunt | -| **IndieHackers** | Post détaillé sur le build process | -| **Dev.to** | Article technique sur l'architecture | -| **Twitter/X (communautés)** | Cibler #BuildInPublic, #SaaS, #i18n | - -### Phase 3 : Croissance (Semaines 4-8) - -| Canal | Action | -|-------|--------| -| **SEO** | Articles de blog : "Comment traduire un Excel sans perdre le format", etc. | -| **YouTube** | Tutoriels et reviews | -| **Partenariats** | Agences de traduction, consultants internationaux | -| **Google Ads** | Mots-clés "translate excel document", "translate powerpoint" | -| **Communautés** | Discord/Slack de développeurs et traducteurs | -| **AppSumo** | Liste Lifetime Deal pour traction initiale | - ---- - -## 4. Stratégie de Contenu - -### Articles de Blog (SEO) - Minimum 5 au lancement - -1. **"Comment traduire un fichier Excel sans perdre la mise en forme"** -2. **"Les 5 meilleurs outils de traduction de documents comparés (2026)"** -3. **"Traduction professionnelle : guide complet pour les PME"** -4. **"Pourquoi DeepL et Google Translate détruisent vos documents Excel"** -5. **"Auto-héberger son outil de traduction : guide complet"** - -### Contenu Réseaux Sociaux (Répétitif) - -| Type | Fréquence | Plateforme | -|------|-----------|-----------| -| Astuce traduction | 2x/semaine | Twitter, LinkedIn | -| Before/After document | 1x/semaine | Twitter, Instagram | -| Thread technique | 1x/2 semaines | Twitter | -| Témoignage client | Quand disponible | Tous | -| Mise à jour produit | Selon releases | Tous | - ---- - -## 5. Stratégie Tarifaire & Positionnement Prix - -### Forfaits actuels (à communiquer) - -| Plan | Prix | Public cible | Message clé | -|------|------|-------------|-------------| -| **Starter** | Prix entry-level | Freelancers, étudiants | "Testez sans risque" | -| **Pro** | Prix milieu | PME, consultants | "Le meilleur rapport qualité-prix" | -| **Business** | Prix premium | Agences, entreprises | "Volume illimité + support dédié" | +**Paquets de crédits** (au-delà des quotas, sans changer d'abonnement) : 50 cr. 5 € · 100 cr. 9 € · 250 cr. 20 € · 500 cr. 35 € · 1 000 cr. 60 € (0,06–0,10 €/page). ### Message tarifaire -- Insister sur le **coût par page** vs traduction humaine (généralement 50-100x moins cher) -- Mettre en avant le **freemium** ou l'**essai gratuit** si disponible -- Comparer avec les coûts des solutions concurrentes +- **Ancrage** : coût par page vs traduction humaine (50–100× moins cher) et vs réfection manuelle après Google Translate. +- **Freemium réel** : le plan Free (2 docs/mois, sans carte) sert d'essai — c'est le « free trial » opérationnel. +- **Transparence** : les prix exacts ci-dessus sont ceux du backend (`models/subscription.py` + `services/pricing_config.py`) ; toute promo est gérée via `data/pricing_overrides.json` (admin). À noter : la section pricing de la landing (`components/pricing-section.tsx`) est un **miroir statique** de ces plans (constante `PLANS`) — aucun prix n'est généré automatiquement, donc tout changement de prix/fonctionnalité doit être répercuté **à la main** dans ce composant et dans ce document le même jour. --- -## 6. KPIs à Suivre +## 3. Actifs Marketing en Place (état 2026-08-29) -| Métrique | Objectif Mois 1 | Objectif Mois 3 | -|----------|----------------|-----------------| -| Visiteurs uniques | 5,000 | 25,000 | -| Inscriptions | 200 | 1,500 | -| Documents traduits | 500 | 5,000 | -| Taux de conversion | 2% | 4% | -| NPS | > 40 | > 50 | -| Revenue mensuel | Variable | Variable | +| Actif | Statut | Emplacement | +|---|---|---| +| Landing page (hero, démo de traduction) | ✅ | `office-translator-landing-page/` (Next.js 15 + Tailwind) | +| Section **Pricing** (4 plans + toggle mensuel/annuel + bannière Enterprise) | ✅ | `components/pricing-section.tsx` | +| Section **FAQ** (10 questions) | ✅ | `components/faq-section.tsx` | +| Section **Preuve sociale** (stats vérifiables : 60+ langues, 7 moteurs, 100 % format, TTL 60 min + 3 témoignages génériques à remplacer par de vrais clients) | ✅ | `components/social-proof-section.tsx` | +| **Capture d'e-mail** (waitlist : e-mail + segment d'intérêt) | ✅ | `components/waitlist-section.tsx` → `POST /api/v1/waitlist` | +| **Analytics** (Vercel Analytics : pages, événements, heatmaps) | ✅ | `app/layout.tsx` (`@vercel/analytics`) | +| Footer corrigé (mention SOC 2 retirée) | ✅ | `app/page.tsx` | + +### À produire avant le lancement (voir §6, checklist) +- 8 captures d'écran réelles de l'interface (héros, upload, résultat côte à côte, sélecteur de moteur, glossaires, dashboard, pricing, profil) +- Vidéo démo 60–90 s + tutoriel 3–5 min +- GIF 15 s du workflow upload → traduction → téléchargement +- OG image 1200×630 + bannière GitHub + infographie avant/après +- Remplacement des 3 témoignages génériques par 3 témoignages clients réels (objectif : 1 par segment P1/P2) --- -## 7. Plan d'Action pour l'Agent Marketing +## 4. Canaux de Lancement -### Checklist Exécutable +### Phase 1 — Pré-lancement (Semaines 1–2) +| Canal | Action | Responsable | Livrable | +|---|---|---|---| +| Landing page | Publication en prod (Vercel ou Docker) avec URL propre `wordly.art` | Dev | URL + OG image | +| Waitlist | Activation `POST /api/v1/waitlist` en prod ; objectif **150 e-mails** avant lancement | Marketing | Compteur `/api/v1/waitlist/count` | +| Product Hunt | Préparation du listing (assets, tagline, maker comment, 5 commentaires amis planifiés) | Marketing | `docs/marketing/launch/product-hunt.md` | +| Reddit | Rédaction de 3 posts (r/SideProject, r/translator, r/smallbusiness) | Marketing | `docs/marketing/launch/reddit-posts.md` | +| Hacker News | Rédaction du Show HN (angle technique : préservation du format) | Dev | `docs/marketing/launch/show-hn.md` | +| X/Twitter | Thread de lancement (12 tweets + GIF) | Marketing | `docs/marketing/launch/x-thread.md` | +| LinkedIn | Post ciblant PME/agences (fondateur) | Fondateur | Post + 2 relances | -- [ ] **Captures d'écran** : Réaliser les 8 captures listées en section 2A -- [ ] **Vidéo courte** : Produire la démo 60-90s (section 2B) -- [ ] **Vidéo tutoriel** : Produire le screencast 3-5 min -- [ ] **Landing page** : Concevoir et publier avec tous les assets -- [ ] **Product Hunt** : Préparer le listing complet -- [ ] **Reddit posts** : Rédiger 5 posts adaptés par subreddit -- [ ] **Show HN** : Écrire la soumission Hacker News -- [ ] **Twitter thread** : Préparer le thread de lancement (10-15 tweets) -- [ ] **Articles SEO** : Rédiger les 5 articles de blog -- [ ] **OG Image** : Créer l'image de partage réseaux sociaux -- [ ] **GIF animé** : Créer le loop de 15s du workflow -- [ ] **Infographie comparatif** : Créer le visuel avant/après -- [ ] **Setup analytics** : Google Analytics + Mixpanel/PostHog -- [ ] **Email sequence** : 5 emails onboarding post-inscription -- [ ] **FAQ page** : Répondre aux 10 questions les plus fréquentes +### Phase 2 — Lancement (Semaine 3) +| Jour | Action | +|---|---| +| J-2 | Test du tunnel complet : inscription → 1ère traduction → upgrade Stripe (mode test) | +| J0 | Lancement Product Hunt (mardi/mercredi), Show HN (matin EST), thread X, cross-post Reddit, post LinkedIn | +| J1 | Suivi des commentaires (réponse < 2 h), relance X, e-mail waitlist n°1 « vous êtes dedans » | +| J+7 | E-mail waitlist n°2 : 3 études avant/après + offre de lancement (−20 % la 1ʳᵉ année, code via `pricing_overrides.json`) | + +### Phase 3 — Croissance (Semaines 4–8) +| Canal | Action | +|---|---| +| SEO | Publication des 5 articles de blog (§5) ; pages « traduire un excel sans perdre le format », « traduire un powerpoint en anglais » | +| YouTube | Tutoriel 3–5 min + review avant/après | +| Partenariats | 5 agences de traduction (commission 15 % la 1ʳᵉ année), 3 consultants internationaux, annuaires SaaS français | +| Google Ads | Campagne test (10 €/jour) sur « traduire excel » / « translate powerpoint » uniquement si CAC < 3× marge mensuelle | +| Communautés | Discord/Slack de traducteurs et de devs (Build in public) | +| AppSumo | Lifetime deal Business (optionnel, à valider après semaine 4) | --- -## 8. Concurrence Directe +## 5. Stratégie de Contenu -| Concurrent | Força | Faiblesse | Notre avantage | -|-----------|-------|-----------|---------------| -| **Google Translate (docs)** | Gratuit, connu | Détruit les formats complexes | Préservation du format | -| **DeepL (docs)** | Qualité de traduction | Cher, formatage limité | Multi-provider + prix | -| **DocTranslator** | Simple | Qualité inégale, publicité | Interface pro + glossaires | -| **Smartcat** | Complet | Complexe, cher | Simplicité + multi-providers | -| **Transifex** | Enterprise | Trop cher pour PME | Prix accessible | +### Articles de blog (SEO) — 5 au lancement, dans `docs/marketing/blog/` +1. **« Comment traduire un fichier Excel sans perdre la mise en forme »** (pilier, 1 500+ mots) +2. **« Les 5 meilleurs outils de traduction de documents comparés (2026) »** (comparatif : nous vs Google, DeepL, Smartling, Transifex, Smartcat) +3. **« Traduction professionnelle de documents : guide complet pour les PME »** +4. **« Pourquoi DeepL et Google Translate détruisent vos documents Excel (et quoi faire) »** +5. **« Auto-héberger son outil de traduction : guide Docker complet »** (capture de l'audience self-hosted) + +### Rythme social (répétitif, 3 h/semaine max) +| Type | Fréquence | Plateforme | +|---|---|---| +| Avant/Après document (anonymisé) | 1×/sem. | X, LinkedIn | +| Astuce traduction | 2×/sem. | X | +| Mise à jour produit / build-in-public | selon releases | X, LinkedIn, IndieHackers | +| Témoignage client | au fil de l'eau | toutes | --- -## 9. Timeline Recommandée +## 6. KPIs & Mesure + +### Mesure (déjà branchée) +- **Vercel Analytics** sur la landing (pages, événements, top pages, referrers). +- **Waitlist** : `GET /api/v1/waitlist/count` + segmentation par intérêt (champ `interest`). +- **Backend** : `/health` (monitoring) ; quotas par plan visibles dans le dashboard admin. + +### Objectifs +| Métrique | Mois 1 | Mois 3 | Source | +|---|---|---|---| +| Visiteurs uniques landing | 5 000 | 25 000 | Vercel Analytics | +| E-mails waitlist | 150 | 1 500 | `/api/v1/waitlist/count` | +| Inscriptions (tous plans) | 200 | 1 500 | BDD utilisateurs | +| Documents traduits | 500 | 5 000 | Journal de jobs | +| Taux de conversion visite → inscription | 2 % | 4 % | Analytics | +| MRR | variable | variable (cible : 300 abonnements payants) | Stripe | +| NPS | > 40 | > 50 | Enquête post-traduction | +| CAC (Google Ads) | — | < 3× marge mensuelle du plan d'entrée | Ads | + +--- + +## 7. Concurrence (actualisé 2026-08) + +| Concurrent | Force | Faiblesse | Notre avantage | +|---|---|---|---| +| **Google Docs (traduction intégrée)** | Gratuit, connu | Détruit les formats complexes, pas de glossaire | Préservation du format + multi-moteurs | +| **DeepL (documents)** | Qualité de traduction | Prix élevé, formatage limité, moteur unique | 7 moteurs au choix, 60+ langues, prix | +| **Smartling / Smartcat** | Complets, TMS | Lourds, chers, surdimensionnés pour une PME | Simplicité + prix accessible | +| **Transifex** | Enterprise i18n | Trop cher, orienté localisation logicielle | Fichiers bureautiques, pas de code | +| **Outils one-shot (DocTranslator, etc.)** | Simple | Qualité inégale, pub, aucune API | Interface pro, glossaires, API Business | + +--- + +## 8. Timeline Recommandée ``` -Semaine 1-2 : Production des assets (captures, vidéos, images) -Semaine 3 : Lancement sur Product Hunt + Reddit + HN + Twitter -Semaine 4 : Articles SEO + contenu evergreen -Semaine 5-6 : Partenariats + communautés + Google Ads -Semaine 7-8 : Optimisation basée sur les premiers retours +S1-2 : Publication de la landing + waitlist + production des assets (captures, vidéos, GIF) +S3 : Lancement (Product Hunt + Show HN + Reddit + X + LinkedIn) + e-mails waitlist +S4 : 5 articles SEO + YouTube + 5 partenariats agences +S5-6 : Google Ads (test) + communautés + relances +S7-8 : Optimisation (A/B du pricing, témoignages réels, LTV) ``` --- -*Ce document doit être mis à jour au fur et à mesure des retours utilisateur et des métriques observées.* +## 9. Checklist Exécutable (état au 2026-08-29) + +- [x] Landing page : sections Pricing, FAQ, Preuve sociale, Waitlist +- [x] Endpoint waitlist (`POST /api/v1/waitlist`, dédoublonné) + compteur +- [x] Analytics (Vercel Analytics) branchée sur la landing +- [x] Footer corrigé (SOC 2 retiré) +- [x] Contenu de lancement rédigé (`docs/marketing/launch/`) +- [x] 5 articles de blog rédigés (`docs/marketing/blog/`) +- [x] Spec des assets visuels (`docs/marketing/launch/assets-spec.md`) +- [x] Doc de suivi des KPIs (`docs/marketing/kpis.md`) +- [ ] Publication de la landing page en production (`wordly.art`) +- [ ] 8 captures d'écran réelles de l'interface +- [ ] Vidéo démo 60–90 s + tutoriel 3–5 min +- [ ] GIF workflow 15 s + OG image 1200×630 +- [ ] 3 témoignages clients réels (remplacer les génériques) +- [ ] Lancement Product Hunt (assets + 5 commentaires planifiés) +- [ ] Séquence e-mail : 5 e-mails post-inscription +- [ ] Configuration GA4 (optionnel, en complément de Vercel Analytics) + +--- + +*Ce document est la source de vérité marketing. Toute modification de prix passe par `data/pricing_overrides.json` (admin) et doit être répercutée ici le même jour.* diff --git a/_bmad-output/audit-securite-2026-08-26.md b/_bmad-output/audit-securite-2026-08-26.md new file mode 100644 index 0000000..a9e41f8 --- /dev/null +++ b/_bmad-output/audit-securite-2026-08-26.md @@ -0,0 +1,93 @@ +# Audit de sécurité — office_translator + +Date : 2026-08-26 · Branche : `production-deployment` · Périmètre : backend FastAPI, routes, middlewares, services, config, déploiement, dépôt git. + +## Critiques + +### C1. Secrets de production commis dans git +`.env.production` est **suivi par git** (confirmé via `git ls-files`) et contient des valeurs réelles : +- `JWT_SECRET_KEY` (ligne 65/81) — permet de forger n'importe quel JWT utilisateur/admin +- `ADMIN_TOKEN_SECRET`, `POSTGRES_PASSWORD`, `GRAFANA_PASSWORD`, `STRIPE_SECRET_KEY` +- `data/provider_settings.json` (suivi) contient un mot de passe SMTP en clair. +- Un fichier `.db` (1) est suivi : hashes de mots de passe et enregistrements utilisateurs potentiels. + +**Action** : rotation de TOUS ces secrets, suppression des fichiers (`git rm --cached`), purge de l'historique (`git filter-repo`), et déplacement hors du dépôt des clés TLS privées présentes à la racine (`*.key`, non suivies mais à côté du code). + +### C2. Traversée de chemin dans l'ingestion par URL +`routes/translate_routes.py:292-312` — le nom de fichier provient du `Content-Disposition` du serveur distant (contrôlé par l'attaquant puisque `file_url` est fourni par l'utilisateur) et est utilisé **non assaini** : `temp_path = config.UPLOAD_DIR / f"{unique_id}_{filename}"`. `filename="../..../evil.xlsx"` écrit hors de `UPLOAD_DIR`. La whitelist d'extensions ne bloque pas `..`. Contraste : l'upload direct est correctement assaini (`middleware/validation.py:239-259`). + +### C3. SSRF par redirection +`routes/translate_routes.py:263-267` — le hostname est vérifié une fois par `_is_ssrf_risk()` (solide par ailleurs), mais la requête utilise `follow_redirects=True` sans revalidation : une URL publique peut rediriger 302 vers `169.254.169.254`, `localhost`, plages privées. Fenêtre TOCTOU DNS-rebinding en plus. + +### C4. Job de cleanup : bug de clé + purge orpheline sans âge minimum +`middleware/cleanup.py:194` lit `metadata["file_path"]` alors que les fichiers sont suivis sous `"input_path"` (`translate_routes.py:837`) → `tracked_paths` toujours vide → **tout** fichier est classé orphelin, et la suppression orpheline n'a **aucune vérification d'âge** (`cleanup.py:236-241`) → boucle de 5 min supprime des fichiers de jobs en cours. DoS/raison de disponibilité. (`protect_file()` existe mais n'est jamais appelé.) + +## Élevées + +### H1. Zip-bomb non contré +Aucune limite de ratio de décompression ni de taille décompressée sur OOXML : `openpyxl.load_workbook`, `docx.Document`, `zipfile.read()` directs (`translators/word_translator.py:834+`, `pptx_translator.py:544+`). Un fichier de <50 MB très compressé → Go en RAM → OOM worker. Seule la taille compressée est vérifiée. + +### H2. Téléchargement / statut : contrôle de propriété défaillant (IDOR) +- `routes/translate_routes.py:1879-1886` : le check de propriété sur `GET /download/{job_id}` n'est effectué que si un utilisateur est authentifié ; un appelant anonyme qui devine/fuit un job_id (12 hex ≈ 48 bits) télécharge le document traduit, et la suppression après 1er téléchargement permet un DoS contre le légitime propriétaire. +- `routes/translate_routes.py:1690-1779` : `GET /translations/{job_id}` n'a **aucun** check de propriété → énumération de statut, noms de fichiers, erreurs. + +### H3. Traduction anonyme +`/translate` accepte les requêtes non authentifiées (`current_user` optionnel, ligne 547) → traitement gratuit au tarif payant, seule la limitation IP s'applique ; combiné au webhook_url, oracle d'egress gratuit. À confirmer si voulu (démo landing ?). + +### H4. Rate limiting contournable via `X-Forwarded-For` +`middleware/rate_limiting.py:249-261` et `routes/admin_routes.py:176-189` font confiance au premier XFF sans proxy de confiance configuré → rotation d'XFF factice pour contourner rate limit et verrouillage brute-force admin (le verrouillage et les sessions admin sont aussi en mémoire par worker, `admin_routes.py:47-52`). + +## Moyennes + +- **M1. XXE/hardening XML** : `lxml.etree.fromstring` sur des parties ZIP non fiables sans `resolve_entities=False` ni defusedxml (`word_translator.py:731,839…`, `pptx_translator.py:553…`). Mitigé par libxml2 ≥2.9, mais à durcir explicitement. +- **M2. Rotation refresh token sans révocation** : `/refresh` (`routes/auth_routes.py:661-742`) ne révoque pas l'ancien refresh token (7 jours de vie, pas de détection de réutilisation). Reset de mot de passe sans révocation des sessions existantes. +- **M3. Fallback PyJWT absent** : jetons signés en base64 **non signé** silencieusement acceptés (`services/auth_service.py:158-169`) ; garde de production OK mais login/verify ne vérifient pas `JWT_AVAILABLE`. +- **M4. `/checkout/sync`** : ownership conditionnel (`services/payment_service.py:174-178`) si session sans `metadata.user_id`. +- **M5. Endpoint legacy batch** (`legacy_routes.py:248-320`) : pas de magic bytes, nombre de fichiers non borné, traitement synchrone → épuisement CPU/RAM. `/metrics` legacy non authentifié. +- **M6. Upload direct** : `await file.read()` charge tout le fichier (≤50 MB) en RAM avant vérification (`middleware/validation.py:116`) ; l'URL stream correctement. +- **M7. Dépendances vulnérables** (`requirements.txt`) : `python-multipart==0.0.9` (CVE-2024-24762, upload !), `fastapi==0.109.0` (ReDoS), `pydantic==2.5.3`, `stripe==7.0.0`. À mettre à jour en priorité. +- **M8. Temp files images** écrits dans le temp système, non couverts par le cleanup, fuités sur exception (`pptx_translator.py:1060-1066`, `word_translator.py:1273-1275`). + +## Faibles + +- Logout révoque un refresh token fourni sans check de propriété (`auth_routes.py:401-410`). +- Flux Google OAuth access_token sans validation d'audience (`auth_routes.py:604-609`). +- Admin mono-facteur mot de passe partagé ; envisager TOTP. +- Erreurs URL qui divulguent `str(e)` + URL interne au client (`translate_routes.py:356-371`). +- Caches mémoire non bornés (`_gc_key_cache`), jobs « processing » jamais purgés. +- Secret webhook Stripe lu à l'import au lieu du runtime (`payment_service.py:26`). + +## Points forts constatés + +- Vérification Stripe webhook correcte (signature + idempotence + body brut). +- JWT : algorithme épinglé HS256, jetons typés, révocation jti Redis, access 15 min. +- Clés API : stockées en SHA-256, haute entropie, expiration/révocation serveur ; glossaires/prompts correctement scopés par `user_id` (pas d'IDOR là). +- Anti-énumération (bcrypt factice constant-time, forgot-password toujours 200). +- Validation magic-bytes + allowlist extensions sur le flux v1 ; `_is_ssrf_risk` fail-closed ; streaming avec cap d'octets côté URL. +- Échec au démarrage en production si secrets manquants / CORS wildcard ; pas de SQL brut (ORM uniquement) ; bcrypt pour les mots de passe. + +## Priorités de remédiation (ordre recommandé) + +1. **Rotation immédiate** de tous les secrets de `.env.production` + purge historique git (C1). +2. Assainir le filename de l'ingestion URL (réutiliser `FileValidator._sanitize_filename`) (C2). +3. Revalider le SSRF à chaque hop de redirection (désactiver `follow_redirects`, suivre manuellement) (C3). +4. Corriger la clé `file_path`→`input_path` + âge minimum pour suppression orpheline (C4). +5. Exiger l'authentification + ownership sur `/download/{job_id}` et `/translations/{job_id}` (H2). +6. Limites de ratio zip (H1) ; XFF/proxy de confiance + verrouillage Redis (H4). +7. Mise à jour `python-multipart`, `fastapi`, `pydantic` (M7). + +--- + +## Suivi des correctifs (2026-08-26) + +**Corrigés au code :** +- ✅ C1 (partie git) : `.env.production`, `.env.ionos`, `data/provider_settings.json`, `translations.db` retirés du suivi git + `.gitignore`. ⚠️ rotation des secrets + purge de l'historique restent à faire manuellement. +- ✅ C2 : nom de fichier assaini dans l'ingestion URL (`_sanitize_url_filename`). +- ✅ C3 : redirections suivies manuellement avec revalidation SSRF à chaque étape (max 5). +- ✅ C4 : clés `input_path`/`file_path`/`output_path` reconnues + délai de grâce de 15 min avant suppression d'un fichier orphelin. +- ✅ H1 : contrôle anti-fichier-piège (`validate_zip_safety`, ratio max 100:1, 1 Go décompressé) sur les flux v1 et legacy. +- ✅ H2 : contrôle d'accès strict sur `/translations/{id}` et `/download/{id}` (propriétaire obligatoire ; jeton secret requis pour les jobs sans compte). +- ✅ H3 (partiel) : les jobs anonymes nécessitent désormais le jeton secret ; la création anonyme reste possible. +- ✅ M7 : `python-multipart` 0.0.9 → 0.0.20, `fastapi` 0.109.0 → 0.109.1. + +**Restent à faire :** rotation des secrets + purge historique git (manuel), H4 (en-tête X-Forwarded-For), M2 (révocation refresh token), M4, M5, M6. diff --git a/_bmad-output/implementation-artifacts/spec-quick-wins-pipeline-llm.md b/_bmad-output/implementation-artifacts/spec-quick-wins-pipeline-llm.md new file mode 100644 index 0000000..a934567 --- /dev/null +++ b/_bmad-output/implementation-artifacts/spec-quick-wins-pipeline-llm.md @@ -0,0 +1,85 @@ +--- +title: 'Quick wins pipeline LLM : prompt, connexions, cache, event loop' +type: 'bugfix' +created: '2026-08-26' +status: 'draft' +context: [] +--- + + + +## Intent + +**Problem:** Le pipeline de traduction LLM souffre de 4 défauts vérifiés : (1) dans les 3 providers LLM, un prompt personnalisé/glossaire **remplace** le prompt par défaut — la paire de langues et les règles métier disparaissent de la requête ; (2) chaque segment traduit ouvre une nouvelle connexion TCP+TLS (`requests.post` nu) ; (3) le cache de traduction Redis/LRU (`services/translation_cache.py`) est écrit et testé mais **jamais branché** — re-traduire un fichier re-facture 100 % des appels ; (4) 7 appels bloquants (SHA-256 de fichiers, DB, disque, sonde réseau Google) s'exécutent directement dans l'event loop et peuvent geler tout le serveur. + +**Approach:** 4 correctifs chirurgicaux dans la couche providers (openai/minimax/deepseek) et le runner de jobs de `translate_routes.py`, sans changement d'interface publique ni des translators. + +## Boundaries & Constraints + +**Always:** L'interface `TranslationProvider` / `TranslationRequest` / `TranslationResponse` reste inchangée ; la paire de langues doit **toujours** figurer dans le system prompt ; le cache reste piloté par env (`REDIS_CACHE_TTL`, `LRU_CACHE_MAXSIZE`, `REDIS_URL`) ; tout échec du cache est silencieux (fallback LRU, puis poursuite sans cache) ; `from_cache=True` sur les réponses servies du cache. + +**Ask First:** modifier `build_full_prompt` (services/glossary_service.py) ; modifier les schémas providers ; introduire un nouveau backend de cache. + +**Never:** Pas de batch multi-segments LLM (chantier suivant) ; pas de refonte de la double couche providers legacy/nouvelle ; pas de modification des translators/ ; pas de nouvel endpoint ; pas de persistance des jobs. + +## I/O & Edge-Case Matrix + +| Scenario | Input / State | Expected Output / Behavior | Error Handling | +|----------|--------------|---------------------------|----------------| +| Prompt custom actif | glossaire et/ou prompt utilisateur | System prompt = prompt par défaut formaté (avec paire de langues) **+** contexte custom en complément | N/A | +| Segment déjà en cache | même texte + langues + provider + hash prompt | Réponse `from_cache=True`, 0 requête HTTP | N/A | +| Segment répété dans un même document | 2e occurrence après succès de la 1ʳᵉ | Servie par le cache (le batch est séquentiel) | N/A | +| Redis indisponible | `get_cache()` init ou `set()` en échec | Fallback LRU RAM puis non-bloquant ; traduction réussie quand même | Log warning, jamais d'exception | +| Provider en erreur | échec API après retries | Rien n'est écrit dans le cache | Erreur existante propagée | +| Fichier 50 Mo + glossaire configuré | job lancé pendant que d'autres requêtes arrivent | SHA-256, zip-safety, DB, settings et sonde Google hors event loop (`asyncio.to_thread`) | Erreurs existantes propagées | + + + +## Code Map + +- `services/providers/openai_provider.py` -- `_build_system_prompt` :120-128 (bug du remplacement), `requests.post` :263, `translate_text` :426-455 (point d'entrée cache) +- `services/providers/minimax_provider.py` -- prompt inline :181-183, `requests.post` :117, `translate_text` :168+ +- `services/providers/deepseek_provider.py` -- prompt inline :164-166, `requests.post` :111, `translate_text` :151+ +- `services/translation_cache.py` -- API prête et testée : `make_cache_key` :45, `hash_prompt` :77, `get_cache()` :402 (Redis auto + fallback LRU) — aucun appelant hors tests +- `routes/translate_routes.py` -- appels bloquants dans coroutines : `calculate_sha256` :858/:871, `validate_zip_safety` :884, `_load_admin_settings` :1174, `get_glossary_terms` :1191, `get_prompt_content` :1206, `_google_cloud_key_valid` :1242 (fait une traduction HTTP de test synchrone) +- `tests/test_providers/` -- 8 fichiers existants, dont `test_minimax_provider.py` (récent) : patterns de mock à réutiliser + +## Tasks & Acceptance + +**Execution:** +- [ ] `services/providers/openai_provider.py` -- (1) `_build_system_prompt` : concaténer le prompt par défaut formaté + le custom au lieu de `return custom_prompt` ; (2) `requests.Session` d'instance réutilisée dans `_make_api_request` ; (3) `translate_text` : lookup `get_cache()` + `make_cache_key(..., custom_prompt_hash=hash_prompt(custom_prompt))` avant l'appel, `cache.set` après succès -- corrige le bug qualité, la latence et le coût +- [ ] `services/providers/minimax_provider.py` -- mêmes 3 changements (prompt construit inline :181-183) +- [ ] `services/providers/deepseek_provider.py` -- mêmes 3 changements (prompt construit inline :164-166) +- [ ] `routes/translate_routes.py` -- envelopper les 7 appels bloquants listés dans la Code Map dans `asyncio.to_thread` -- l'event loop reste réactif pendant les jobs +- [ ] `tests/test_providers/` -- nouveaux tests couvrant la matrice I/O : le system prompt contient la paire de langues **et** le custom (×3 providers) ; cache hit → 0 appel HTTP et `from_cache=True` ; erreur provider → rien mis en cache ; Redis down → traduction quand même + +**Acceptance Criteria:** +- Given un custom_prompt sans mention de langue, when traduction via openai/minimax/deepseek, then le payload contient « from {source} to {target} » et le contenu custom. +- Given un segment déjà en cache (même clé), when re-traduit, then aucune requête HTTP n'est émise vers l'API LLM. +- Given Redis down, when traduction, then réponse normale via LRU ou sans cache, sans exception remontée au job. +- Given la suite de tests existante, when `pytest -x`, then 0 régression. + +## Spec Change Log + +## Design Notes + +Concaténation du prompt (les 3 providers) : + +```python +system_prompt = DEFAULT_TRANSLATION_PROMPT.format( + source_lang=source_lang_name, target_lang=target_lang_name +) +if custom_prompt: + system_prompt += ( + "\n\nAdditional context and instructions from the user " + f"(comply without overriding the language pair above):\n{custom_prompt}" + ) +``` + +La `requests.Session` est créée dans `__init__` du provider (le pooling urllib3 est thread-safe ; les translators appellent `translate_text` depuis 6 threads). Clé de cache : réutiliser `make_cache_key(text, target_language, source_language, self._provider_name, custom_prompt_hash=hash_prompt(custom_prompt))` — le hash du prompt isole déjà les glossaires différents ; `user_id` absent des metadata aujourd'hui → valeur par défaut « anon » (partage inter-utilisateurs acceptable : mémoire de traduction standard, TTL 24 h). + +## Verification + +**Commands:** +- `pytest tests/test_providers/ -x` -- expected: succès, nouveaux tests inclus +- `pytest -x` -- expected: succès complet, 0 régression diff --git a/_bmad-output/implementation-artifacts/spec-securite-c1-c4.md b/_bmad-output/implementation-artifacts/spec-securite-c1-c4.md new file mode 100644 index 0000000..0c9bf7e --- /dev/null +++ b/_bmad-output/implementation-artifacts/spec-securite-c1-c4.md @@ -0,0 +1,25 @@ +--- +status: done +created: 2026-08-26 +title: Correctifs sécurité critique C1–C4 +--- + +# Spec : Correctifs sécurité C1–C4 + +## Contexte +Audit de sécurité du 2026-08-26 ([rapport](../audit-securite-2026-08-26.md)). Correction des 4 constats critiques. + +## Tâches +1. **C1 — Secrets suivis par git** : `git rm --cached` sur `.env.production`, `.env.ionos`, `data/provider_settings.json`, `translations.db` ; compléter `.gitignore`. (Rotation des secrets + purge d'historique = action manuelle utilisateur, hors scope code.) +2. **C2 — Path traversal URL** : dans `routes/translate_routes.py::download_from_url`, assainir le filename issu de `Content-Disposition`/URL (Path().name, contrôle-chars, length cap, fallback `downloaded_file`). +3. **C3 — SSRF par redirection** : remplacer `follow_redirects=True` par une boucle manuelle (≤5 hops) qui revalide schéma + `_is_ssrf_risk()` à chaque hop. +4. **C4 — Cleanup destructeur** : dans `middleware/cleanup.py::cleanup`, lire toutes les clés de chemin (`input_path`, `file_path`, `output_path`) et n'appliquer la suppression orpheline qu'au-delà d'un âge plancher (`orphan_grace_seconds`, défaut 900 s). + +## Critères d'acceptation +- **AC1** : Étant donné un `Content-Disposition: filename="../../evil.xlsx"`, quand `download_from_url` s'exécute, alors le fichier est écrit dans `UPLOAD_DIR` avec un nom sans traversée. +- **AC2** : Étant donné une URL publique qui redirige (302) vers `http://169.254.169.254/`, quand `download_from_url` s'exécute, alors une erreur `ssrf_blocked` est levée. +- **AC3** : Étant donné un fichier récent (< 15 min) non tracé dans Redis, quand `cleanup()` s'exécute, alors le fichier n'est PAS supprimé ; au-delà du plancher il l'est. +- **AC4** : `git ls-files` ne contient plus `.env.production`, `.env.ionos`, `translations.db`, `data/provider_settings.json`. + +## Tests +- Tests unitaires pour le filename sanitizer, la boucle de redirection, et la logique orpheline du cleanup. diff --git a/config.py b/config.py index 5a8d26a..2c220a8 100644 --- a/config.py +++ b/config.py @@ -70,8 +70,8 @@ class Config: # ============== Quality Layer (L0) ============== # Track A1 of the dev plan — observability only, no behavior change. - # Set to "true" to enable. Default: false (opt-in). - QUALITY_L0_ENABLED = os.getenv("QUALITY_L0_ENABLED", "false").lower() == "true" + # Enabled by default since 2026-08-29: log-only, never blocks a job. + QUALITY_L0_ENABLED = os.getenv("QUALITY_L0_ENABLED", "true").lower() == "true" # Number of text samples to extract from the output file for L0 analysis. # Keep small to avoid overhead. 20 is enough to catch language confusion. QUALITY_L0_SAMPLE_SIZE = int(os.getenv("QUALITY_L0_SAMPLE_SIZE", "20")) @@ -117,6 +117,22 @@ class Config: # Set to false to use the legacy aggressive-shrink strategy (NOT recommended). PDF_SMART_FIT_ENABLED = os.getenv("PDF_SMART_FIT_ENABLED", "true").lower() == "true" + # ============== Scanned PDF OCR (Mistral) ============== + # Image-only PDFs have no extractable text layer. When a PDF looks + # scanned, it is routed to the Mistral OCR API to recover the text + # before translation (output: clean re-typeset PDF). + # Pricing reference: ~$1 / 1000 pages — set MISTRAL_OCR_ENABLED=false + # to disable and reject scanned PDFs with an explicit error instead. + MISTRAL_API_KEY = os.getenv("MISTRAL_API_KEY", "").strip() + MISTRAL_OCR_MODEL = os.getenv("MISTRAL_OCR_MODEL", "mistral-ocr-latest") + MISTRAL_OCR_TIMEOUT = int(os.getenv("MISTRAL_OCR_TIMEOUT", "180")) + MISTRAL_OCR_ENABLED = os.getenv("MISTRAL_OCR_ENABLED", "true").lower() == "true" + # A page with fewer extractable characters than this is text-poor; it + # counts as a scan page only when raster images also cover most of it. + SCANNED_PDF_MIN_CHARS_PER_PAGE = int( + os.getenv("SCANNED_PDF_MIN_CHARS_PER_PAGE", "100") + ) + # ============== API Configuration ============== API_TITLE = "Document Translation API" diff --git a/core/languages.py b/core/languages.py new file mode 100644 index 0000000..b68be4d --- /dev/null +++ b/core/languages.py @@ -0,0 +1,137 @@ +""" +Language display names for LLM prompts and UI labels. + +Single source of truth for code → English name across providers. The map +covers every code exposed by /api/v1/languages (SUPPORTED_LANGUAGES), so +prompts say "Translate to Tagalog" instead of "Translate to tl" — LLMs +translate noticeably better with full language names. +""" + +from typing import Dict + +LANGUAGE_NAMES: Dict[str, str] = { + "af": "Afrikaans", + "sq": "Albanian", + "am": "Amharic", + "ar": "Arabic", + "hy": "Armenian", + "az": "Azerbaijani", + "eu": "Basque", + "be": "Belarusian", + "bn": "Bengali", + "bs": "Bosnian", + "bg": "Bulgarian", + "ca": "Catalan", + "ceb": "Cebuano", + "zh": "Chinese", + "zh-CN": "Chinese (Simplified)", + "zh-TW": "Chinese (Traditional)", + "co": "Corsican", + "hr": "Croatian", + "cs": "Czech", + "da": "Danish", + "nl": "Dutch", + "en": "English", + "eo": "Esperanto", + "et": "Estonian", + "fi": "Finnish", + "fr": "French", + "fy": "Frisian", + "gl": "Galician", + "ka": "Georgian", + "de": "German", + "el": "Greek", + "gu": "Gujarati", + "ht": "Haitian Creole", + "ha": "Hausa", + "haw": "Hawaiian", + "he": "Hebrew", + "hi": "Hindi", + "hmn": "Hmong", + "hu": "Hungarian", + "is": "Icelandic", + "ig": "Igbo", + "id": "Indonesian", + "ga": "Irish", + "it": "Italian", + "ja": "Japanese", + "jv": "Javanese", + "kn": "Kannada", + "kk": "Kazakh", + "km": "Khmer", + "rw": "Kinyarwanda", + "ko": "Korean", + "ku": "Kurdish", + "ky": "Kyrgyz", + "lo": "Lao", + "la": "Latin", + "lv": "Latvian", + "lt": "Lithuanian", + "lb": "Luxembourgish", + "mk": "Macedonian", + "mg": "Malagasy", + "ms": "Malay", + "ml": "Malayalam", + "mt": "Maltese", + "mi": "Maori", + "mr": "Marathi", + "mn": "Mongolian", + "my": "Myanmar (Burmese)", + "ne": "Nepali", + "no": "Norwegian", + "ny": "Nyanja (Chichewa)", + "or": "Odia (Oriya)", + "ps": "Pashto", + "fa": "Persian (Farsi)", + "pl": "Polish", + "pt": "Portuguese", + "pa": "Punjabi", + "ro": "Romanian", + "ru": "Russian", + "sm": "Samoan", + "gd": "Scots Gaelic", + "sr": "Serbian", + "st": "Sesotho", + "sn": "Shona", + "sd": "Sindhi", + "si": "Sinhala", + "sk": "Slovak", + "sl": "Slovenian", + "so": "Somali", + "es": "Spanish", + "su": "Sundanese", + "sw": "Swahili", + "sv": "Swedish", + "tl": "Filipino (Tagalog)", + "tg": "Tajik", + "ta": "Tamil", + "tt": "Tatar", + "te": "Telugu", + "th": "Thai", + "tr": "Turkish", + "tk": "Turkmen", + "uk": "Ukrainian", + "ur": "Urdu", + "ug": "Uyghur", + "uz": "Uzbek", + "vi": "Vietnamese", + "cy": "Welsh", + "xh": "Xhosa", + "yi": "Yiddish", + "yo": "Yoruba", + "zu": "Zulu", +} + + +def language_name(code: str) -> str: + """Full English name for a language code; case-insensitive lookup. + + Falls back to the code itself for unknown values (and "" for auto/None, + which callers use to mean "detect the source language"). + """ + if not code or code == "auto": + return "" + name = LANGUAGE_NAMES.get(code) + if name is None: + name = LANGUAGE_NAMES.get(code.split("-")[0].lower(), code) + return name diff --git a/data/provider_settings.json b/data/provider_settings.json deleted file mode 100644 index 5bf3333..0000000 --- a/data/provider_settings.json +++ /dev/null @@ -1,78 +0,0 @@ -{ - "google": { - "enabled": true, - "api_key": null, - "base_url": null, - "model": null, - "timeout": 30, - "max_retries": 3 - }, - "google_cloud": { - "enabled": false, - "api_key": null, - "base_url": null, - "model": null, - "timeout": 30, - "max_retries": 3 - }, - "deepl": { - "enabled": false, - "api_key": null, - "base_url": null, - "model": null, - "timeout": 30, - "max_retries": 3 - }, - "openai": { - "enabled": false, - "api_key": null, - "base_url": null, - "model": "gpt-4o-mini", - "timeout": 30, - "max_retries": 3 - }, - "ollama": { - "enabled": false, - "api_key": null, - "base_url": "http://localhost:11434", - "model": "gpt-oss:20b", - "timeout": 30, - "max_retries": 3 - }, - "openrouter": { - "enabled": true, - "api_key": null, - "base_url": null, - "model": "google/gemini-3.5-flash", - "timeout": 30, - "max_retries": 3 - }, - "openrouter_premium": { - "enabled": false, - "api_key": null, - "base_url": null, - "model": "anthropic/claude-sonnet-4.6", - "timeout": 30, - "max_retries": 3 - }, - "zai": { - "enabled": false, - "api_key": null, - "base_url": "https://api.x.ai/v1", - "model": "grok-2-1212", - "timeout": 30, - "max_retries": 3 - }, - "smtp": { - "enabled": true, - "host": "smtp.ionos.fr", - "port": 587, - "username": "admin@wordly.art", - "password": "Esenaw,121151", - "from_email": "admin@wordly.art", - "use_tls": true - }, - "fallback_chain": "google,deepl,openai,ollama,openrouter,zai", - "fallback_chain_classic": "google,deepl", - "fallback_chain_llm": "ollama,openai,openrouter,zai" -} \ No newline at end of file diff --git a/docs/AUDIT_APPROFONDIE_2026-08-29.md b/docs/AUDIT_APPROFONDIE_2026-08-29.md new file mode 100644 index 0000000..c76f2b9 --- /dev/null +++ b/docs/AUDIT_APPROFONDIE_2026-08-29.md @@ -0,0 +1,96 @@ +# Passe approfondie n°2 — Traduction, mise en page, formats, admin & benchmark + +**Date :** 29 août 2026 (2ᵉ passe) · **Statut : corrections appliquées et testées** + +Méthode : revue complète des 4 traducteurs (word/excel/pptx/pdf) ligne à ligne (35 constats), benchmark web actualisé de 9 concurrents, puis correction des P1. + +--- + +## 1. Corrections appliquées aujourd'hui (toutes testées) + +### Qualité de traduction & mise en page + +| # | Correctif | Impact | +|---|---|---| +| 1 | **Word/PPTX→Word : fusion des runs de même formatage** — une phrase éclatée en runs adjacents identiques (découpes rsid, correcteur ortho.) est désormais traduite en UNE unité ; la traduction s'écrit dans le 1ᵉʳ run, les frères sont vidés, les espaces de bord préservés. Les changements de formatage (gras au milieu) restent des unités séparées avec leur contexte. | **Plus gros écart qualité vs DeepL comblé** : fini les fragments hors contexte (« This is » / « very » / « important » en 3 appels) | +| 2 | **PPTX : les traductions de graphiques atteignent enfin le fichier** — l'ancien code assignait `ChartPart.blob` (propriété en lecture seule de python-pptx) : l'erreur était avalée et les titres/séries de graphiques n'étaient JAMAIS écrits. Réécriture du XML des graphiques dans le ZIP de sortie (même mécanisme que Word) + test de non-régression sur un VRAI graphique | Titres/axes/séries traduits pour de vrai | +| 3 | **Excel : le renommage d'onglets ne casse plus les formules** — openpyxl ne réécrit pas les références ; ajout de la réécriture dans : formules de cellules (multi-réfs, refs 3D, quoted/unquoted), noms définis, validations de données, mises en forme conditionnelles. Test dédié (`test_excel_sheet_rename_refs.py`) | `=SUM(Ventes!A3:A4)` → `=SUM(Sales!A3:A4)` au lieu de `#REF!` | +| 4 | **Word : commentaires/bulles traduits** (`word/comments.xml`, même mécanisme post-save que les notes de bas de page) | Cohérence vs DeepL pour les relecteurs | +| 5 | **Word : les zones de texte ne sont plus traduites 2×** (ensemble `seen_run_elements` partagé — coût API ÷2 sur ces éléments) | Coût + cohérence | +| 6 | **Word RTL : l'alignement n'est plus forcé à droite** — centré/justifié préservé, seul « gauche » devient « droite » | Titres RTL corrects | +| 7 | **PDF : gras/italique restitués** — sélection `hebo`/`heit`/`hebi` selon les flags du bloc (tout était redessiné en `helv` régulier) | Hiérarchie visuelle conservée | +| 8 | **PDF : les cellules de tableaux ne fusionnent plus** — `_is_table_cell` était calculé puis jamais lu ; deux lignes consécutives d'une même colonne étaient jointes en un paragraphe | Structure des tableaux préservée | +| 9 | **PDF : un bloc inchangé n'est plus réécrit** — si la traduction est identique (échec fournisseur ou déjà dans la langue cible), on ne rédacte PAS : typo et polices incorporées d'origine conservées (au lieu de tout redessiner en police de substitution). Corrige au passage un doublon de liens hypertexte dans ce scénario | Moins de dégradation, pas de régression | +| 10 | **PDF : stats attempted/changed remontent** (+ propagation du chemin fallback pdf2docx) et la route applique le garde-fou `attempted==0`/`changed==0` aussi au PDF (une panne totale du moteur ne produit plus un job « réussi » non traduit) | Détection d'échec | +| 11 | **Prompts LLM : noms de langues complets** — nouvelle source unique `core/languages.py` (107 langues) utilisée par openai/deepseek/minimax + service legacy ; « Translate to Tagalog » au lieu de « Translate to tl » | Qualité sur langues rares | + +### Page d'admin (revue + améliorations) + +| # | Amélioration | Détail | +|---|---|---| +| A1 | **Réglages OCR Mistral dans l'admin** — nouvelle section `mistral` dans `SettingsConfig` (clé/modèle/timeout/activation), fusion env-var comme les autres moteurs, badge « clé dans .env », bouton **Tester** (validation réelle de la clé via `GET /v1/models`) | L'OCR PDF scannés se configure depuis l'UI, plus seulement via .env | +| A2 | **Dashboard : statut des 8 moteurs + OCR** — le panneau providers ne montrait que Google ; il liste désormais DeepL, OpenRouter éco/premium, OpenAI, Grok, Google Cloud et « OCR Mistral » avec état dérivé de la configuration (sans appel réseau) et tooltip explicite ; badge « PDF scannés refusés » si OCR non configuré | Vérifié en direct : 8 statuts + erreurs claires « Clé absente (X) » | +| A3 | L'OCR du pipeline lit les réglages admin > env (`set_ocr_config`), avec garde anti-footgun (un settings.json fraîchement sauvegardé ne désactive pas l'OCR configuré par env) | Cohérence prod | + +--- + +## 2. Benchmark concurrentiel (résumé) — positionnement validé + +Sources fraîches 2026-08-29 (DeepL changelog, pricing Google Cloud, Trustpilot/Reddit, annonces Mistral/Anthropic/OpenAI). Détails et URL dans le rapport d'agent. + +- **Grille tarifaire Free/9/19/49 € : compétitive et bien échelonnée** — 9 € sous DeepL mensuel effectif, 19 € sans concurrent direct à ce prix avec choix de moteur, 49 € ~30-40 % sous DeepL Advanced. Recommandations : mettre les packs de crédits en avant face à l'ancre Google 0,08 $/page ; futur palier équipe ~99-149 € (zone vide avant Smartcat 100 $/mois). +- **Top plaintes utilisateurs DeepL/Google** (= nos angles d'attaque) : mise en page cassée sur docs complexes (n°1), PDF scannés refusés/gérés en texte brut, truncature silencieuse sur longs docs, plafonds de taille, aucun outil de relecture avant export, facturation/annulation friction. +- **Différenciateurs que nous avons déjà et qu'eux n'ont pas** : multi-moteurs par document, OCR PDF scannés intégré (DeepL web refuse ; Google refuse), 60+ langues à 9 €. +- **Marque : risque à surveiller** — « Wordly® » est une marque déposée par Wordly Inc. (wordly.ai, interprétation IA) ; vérification juridique recommandée avant d'investir dans la marque. + +### Gap map vs DeepL (après les correctifs du jour) + +| Fonction | DeepL | Wordly.art | Statut | +|---|---|---|---| +| Segmentation phrase avec protection du format inline | Oui (tags) | **Oui** (fusion de runs) | ✅ comblé aujourd'hui | +| Glossaires | Natifs (API, jusqu'à 5/requête) | Prompt LLM uniquement — **DeepL/Google : ignorés** | ⚠️ reste à faire (API DeepL directe) | +| Formalité (formel/informel) | Oui (Pro) | Non | 📋 quick win (prompt) | +| Variantes régionales (pt-BR/PT, fr-CA, de-CH) | GA juillet 2026 | Codes acceptés, pas de contrôle fin | 📋 quick win (prompt) | +| Mémoire de traduction | Pillier 2026 (Customization Hub) | Cache mémoire par process seulement ; le cache Redis TM codé n'est pas branché | 📋 à activer | +| Scores de confiance / QA | Non (prosumer) | L0/L1 codés, désactivés ; L2 réparé aujourd'hui | 📋 à activer | +| Sortie bilingue | Non | Non | 📋 quick win (demande forte) | +| Édition/relecture avant export | Non | Non | 📋 médium — différenciateur majeur vs DeepL | +| XLIFF (post-édition CAT) | API juillet 2026 | Non | 📋 médium (agences) | + +--- + +## 3. Reste à faire (priorisé, non fait aujourd'hui) + +**Quick wins (quelques heures chacun)** +1. Formalité + variantes régionales : option par job → prompt LLM (et `formality` DeepL quand l'API directe arrivera). +2. Sortie bilingue docx (paragraphes source+target en regard) — les segments existent déjà, c'est un rendu. +3. Activer le cache Redis TM existant (`services/translation_cache.py`) : cohérence terminologique + coûts ↓. +4. Activer les couches qualité L0/L1 (réparées) en log-only, afficher un score de confiance par document. + +**Médium** +5. Glossaires sur moteurs non-LLM : passer DeepL en API HTTP directe (`glossary_id`, `formality`, `tag_handling`). +6. Contexte document dans les prompts LLM (titre/section courante en préfixe) + batch par liste JSON numérotée (~15× moins de requêtes). +7. Rapport QA post-traduction (écarts de nombres, segments non traduits, glossaire violé). +8. XLIFF 1.2/2.x export/import (agences). + +**Plus gros / roadmap** +9. Éditeur de relecture côte à côte avant téléchargement (le vrai différenciateur pro). +10. Espaces de travail équipes (rôles, glossaires/TM partagés) → palier 99-149 €. +11. Polices par script cible (CJK/arabe) dans Word/PPTX (`eastAsia`/`cs`) et PDF (matrice de polices) ; annotations FreeText et texte pivoté en PDF. +12. Formats IDML/DITA (DeepL API les a ajoutés en juillet 2026 — créneau agences). + +--- + +## 4. Vérifications + +- **Suite complète : 1150 passed / 0 failed / 157 skipped** (les 6 tests désélectionnés sont des tests « RealAPI » réseau qui échouent uniquement parce que l'endpoint Google gratuit est momentanément bloqué depuis cette machine — ils passent quand le réseau coopère). +- **Réparations d'infrastructure de test au passage** (préexistantes, démasquées par la remise en route de la suite) : + - `requirements.txt` : httpx désormais borné `<0.28` (0.28 a supprimé `Client(app=)`, cassait 270 tests via starlette TestClient) → ~270 tests récupérés ; + - `tests/test_metrics.py` : fixture Prometheus réécrit (les compteurs vont sur un registre frais, plus de `Duplicated timeseries` quand l'app a déjà été importée) ; + - `tests/test_scanned_pdf_ocr.py` : le test e2e passe par `set_ocr_config()` (immunisé contre le rechargement du module config par un autre test). +- Suite traducteurs seule : 196/196 (dont 8 nouveaux tests PDF qualité, test graphique PPTX sur vrai fichier, 4 tests renommage Excel, 2 tests fusion runs Word, 2 tests gating mis à jour). +- Frontends : `tsc --noEmit` OK sur `frontend/` et `office-translator-landing-page/`. +- Dashboard admin testé en direct (8 statuts moteurs/OCR avec erreurs claires). +- Note : le Google gratuit (scraping) était bloqué depuis cette machine au moment des tests réseau (`TranslationNotFound`) — le garde-fou `changed==0` l'a correctement détecté ; c'est la fragilité connue qui justifie DeepL/Cloud pour les plans payants. + +*Rapports complets des agents : analyse 35 constats (volet code) et benchmark 9 concurrents avec sources (volet marché) disponibles dans l'historique de session.* diff --git a/docs/AUDIT_TRADUCTION_2026-08-29.md b/docs/AUDIT_TRADUCTION_2026-08-29.md new file mode 100644 index 0000000..c30771e --- /dev/null +++ b/docs/AUDIT_TRADUCTION_2026-08-29.md @@ -0,0 +1,146 @@ +# Audit fonctionnel — Pipeline de traduction (wordly.art) + +**Date :** 29 août 2026 · **Périmètre :** backend de traduction (routes → providers → translators docx/xlsx/pptx/pdf) + benchmark concurrentiel web +**Méthode :** lecture du code, traduction réelle de fichiers de test via le pipeline de production (provider Google, fr→en), vérification programmatique des sorties, revue de la suite pytest, recherche web concurrents/bonnes pratiques. + +--- + +## 0. Corrections appliquées (même jour) + +| # | Correction | Fichiers | Vérification | +|---|---|---|---| +| 1 | **zh-CN/zh-TW acceptés** : validation insensible à la casse, forme canonique propagée (`validate("zh-cn")` → `"zh-CN"`, alias `chinese`/`tw` conservés) | `middleware/validation.py` · `routes/translate_routes.py` (valeurs de retour utilisées) | 11 tests `tests/test_language_validation.py` ✅ | +| 2 | **NameError `current_user`** dans `_run_translation_job` : nouveau helper `_tier_from_plan_str()` (gère `"PlanType.PRO"` et `"pro"`) ; les couches qualité L0/L1/L2 reçoivent enfin le bon tier | `routes/translate_routes.py` | Suite pytest ✅ | +| 3 | **Crash libmagic sous Windows** : `import magic` désactivé sur win32, dégradation magie-bytes (PDF/ZIP) ; **la suite pytest ne bloque plus** | `middleware/validation.py` | 207 tests passent (vs hang avant) | +| 4 | **OCR Mistral pour PDF scannés** : détection (page quasi sans texte ET couverte ≥50 % par une image), client API `services/mistral_ocr.py` (chunks de 8 pages, retries, erreurs typées), chemin OCR → traduction → PDF propre ; erreur explicite sans `MISTRAL_API_KEY` | `services/mistral_ocr.py` (nouveau) · `translators/pdf_translator.py` · `config.py` · `.env.example` | 12 tests `tests/test_scanned_pdf_ocr.py` + E2E réel (OCR mocké, traduction Google réelle) ✅ | + +**Total : 207 tests passent** (`tests/test_translators` + les 2 nouveaux fichiers). La sortie OCR est un PDF re-mis en page propre : les pages scannées étant des images, la disposition d'origine ne peut pas être réécrite en place. + +--- + +## 1. Synthèse exécutive + +Le pipeline fonctionne : un document Word, Excel ou PowerPoint envoyé sur `POST /api/v1/translate` est traduit, mis en forme à l'identique (gras, tableaux, formules, styles) et reste téléchargeable. Les tests réels effectués passent à **14/14**, et les **106 tests unitaires** des traducteurs Office passent en 3,7 s. + +En revanche, l'audit a confirmé **4 bugs fonctionnels** dont un bloquant pour le chinois (rejeté côté API alors que l'UI le propose), **2failles qualité structurelles** (glossaire ignoré par les providers non-LLM, phrase découpée en « runs » traduite morceau par morceau), et un point d'architecture fragile (jobs perdus au redémarrage). Les couches qualité L0/L1/L2 déjà développées sont désactivées par défaut et la L2 est cassée par un bug (`NameError`). + +Le benchmark web montre que le positionnement est bon (LLM + préservation du format + prix), mais que les différenciateurs attendus en 2026 sont : **PDF scannés via OCR** (refusés par DeepL/Azure), **sortie bilingue**, **mémoire de traduction persistante** (le code existe mais n'est pas branché) et **scores de qualité visibles**. + +--- + +## 2. Ce qui fonctionne (vérifié en conditions réelles) + +Script de test : `temp/audit/audit_functional.py` (fichiers générés puis traduits via `WordTranslator`/`ExcelTranslator`/`PowerPointTranslator` + provider Google de production). + +| Vérification | Résultat | +|---|---| +| DOCX — titre, paragraphes traduits | ✅ « Rapport financier annuel » → « Annual financial report » | +| DOCX — phrase coupée en 3 runs (gras au milieu) | ✅ cohérente dans ce cas, gras conservé sur le bon run | +| DOCX — nombres/monnaies préservés | ✅ « 4 500 000 euros en 2025 » intact | +| DOCX — tableau traduit | ✅ | +| XLSX — cellules, en-têtes, mois | ✅ « Janvier » → « January » | +| XLSX — **formules préservées** | ✅ `=SUM(B3:B4)` inchangée | +| PPTX — titre + puces | ✅ « Stratégie commerciale 2026 » → « Commercial strategy 2026 » | +| Statistiques anti-échec (`attempted/changed`) | ✅ remontées au job | + +Points d'architecture solides constatés : validation magic bytes + zip-bomb, protection SSRF sur `file_url`, quotas mensuels atomiques, progression temps réel (Redis, TTL 2h), webhooks signés par jeton par-job, watermark gratuit, RTL (ar/he/fa), notes de bas de page, SmartArt, graphiques (ré-injection ZIP), noms de feuilles Excel reécrits avec mise à jour des références. + +--- + +## 3. Bugs confirmés + +### P1 — Le chinois est rejeté comme langue cible (bloquant, visible utilisateur) — ✅ CORRIGÉ +`middleware/validation.py:488` met le code en minuscules (`zh-cn`) mais `SUPPORTED_LANGUAGES` contient `"zh-CN"`/`"zh-TW"` en casse mixte (`:349-350`) → `LanguageValidator.validate("zh-CN")` lève une erreur → **400 systématique**. L'UI propose pourtant `zh-CN`/`zh-TW` (`frontend/src/app/dashboard/translate/useTranslationConfig.ts:31-32`). De plus la valeur normalisée retournée par `validate()` est ignorée dans `routes/translate_routes.py:796,806` (alias `chinese`→`zh-CN` jamais propagés). +**Fix appliqué (voir §0).** + +### P1 — `NameError: current_user` : la couche qualité L2 ne fonctionne jamais — ✅ CORRIGÉ +Dans `_run_translation_job` (`routes/translate_routes.py:1574-1575, 1592-1593, 1611`), `current_user` n'existe pas (signature `:1105-1120`). L'`NameError` est avalé par les `except` : quand `QUALITY_L2_ENABLED=true`, le juge qualité Pro **échoue silencieusement à chaque job** ; les métriques `record_translation_retry` L0/L1 sont mortes aussi. +**Fix appliqué (voir §0).** + +### P2 — Glossaire silencieusement ignoré selon le provider +Le glossaire n'est injecté que via `metadata["custom_prompt"]`, lu uniquement par openai/deepseek/minimax. Avec **google, deepl ou google_cloud, un utilisateur Pro qui paie pour le glossaire n'a aucune application des termes** — sans avertissement. `build_full_prompt` avec glossaire seul produit en outre un prompt système **sans instruction de traduction** (le prompt par défaut est remplacé : `services/providers/openai_provider.py:124-125`), ce qui fragilise la qualité même en LLM. +**Fix :** fusionner glossaire + prompt par défaut au lieu de remplacer ; refuser ou avertir quand provider non-LLM + glossary_id ; à terme, utiliser les glossaires natifs DeepL. + +### P2 — URL de téléchargement cassée sur `/translate-batch` (legacy) +`routes/legacy_routes.py:304` renvoie `/api/v1/download/{output_filename}` alors que la route exige un id `tr_*` (`translate_routes.py:26`) → 400 INVALID_JOB_ID pour tout client de l'endpoint batch. + +### P2 — Crash natif Windows dans le chemin « format loss » PDF + violation de couche — ✅ CORRIGÉ +`translators/pdf_translator.py:69` (`_record_format_loss_metric`) importe `middleware.metrics` au milieu de la traduction PDF. L'import déclenche `middleware/__init__.py` → `middleware/validation.py:7` → `import magic` → **`python-magic`/libmagic plante en « access violation » sous Windows** (crash natif non rattrapable par le `try/except` du wrapper). Conséquences : (a) la suite pytest **bloque indéfiniment** sur `tests/test_translators/test_b3_5_pdf_smart_fit.py::test_tier_3_4_limit_to_two_shrink_steps` ; (b) en production Windows, tout PDF avec perte de format (overflow → placeholder `[translation overflow]`) risque le même sort. C'est aussi une violation de couche : `translators/` ne devrait pas dépendre de `middleware/`. +**Fix appliqué (voir §0) : `import magic` désactivé sous Windows + dégradation magie-bytes ; l'import paresseux `middleware.metrics` dans pdf_translator est désormais inoffensif. La répartition translators/→core/ reste à faire (P3).** + +### P3 — Divers +- Échec de traduction = **texte source renvoyé silencieusement** (chaque provider). Le garde-fou `changed == 0` (`translate_routes.py:1480`) ne bloque que l'échec total : un document traduit à 10 % passe (warning seul, `:1490`). L'utilisateur paie pour un document partiel. +- Modèle OpenRouter réécrit silencieusement (`:1182-1183`) : la config admin `deepseek/deepseek-v3.2` est forcée vers `google/gemini-3.5-flash`. +- `max_tokens=500` dans le provider legacy `OpenAITranslationProvider` (`services/translation_service.py:1071`) peut tronquer un paragraphe long. +- `OllamaTranslationProvider.list_models` défini 2 fois (l'instance method est masquée par le staticmethod, `translation_service.py:649` vs `:693`). +- Provider « zai » pointe par défaut vers xAI/Grok (`translate_routes.py:1319-1329`) — dénomination trompeuse. +- Code mort : `WebLLMTranslationProvider`, `_GoogleCloudWithFallback`, `services/translation_cache.py` (cache Redis TM — voir §5), chaîne de fallback `services/providers/fallback.py` non branchée sur la route principale. + +--- + +## 4. Risques qualité structurels (traduction) + +1. **Segmentation par run (docx/pptx).** Chaque `` est traduit indépendamment (`translators/word_translator.py:1147-1220`). Dès qu'une phrase est découpée par du gras, une couleur, une correction orthographique (rsid) ou un saut de champ, les morceaux sont traduits séparément : l'ordre des mots cible peut rendre la phrase incohérente (cas fr→en avec adjectifs). Le test est passé parce que la découpe tombait bien ; c'est fragile par construction. C'est **l'écart principal face à DeepL** qui traduit au niveau document/phrase. + **Piste :** traduire au niveau paragraphe avec masquage de placeholders (`<0>`, `<1>`…) pour protéger les frontières de mise en forme, puis redistribuer — technique standard validée (cf. recherche web, « tag protection »). +2. **Moteur par défaut = Google Translate gratuit non officiel** (deep_translator, scraping). Limité à 5 000 caractères/requête, non contractuel, risque de blocage/ToS, qualité plafonnée. Acceptable en free tier, risqué comme moteur par défaut des offres payantes. +3. **Aucune détection de langue réelle** : `source_lang=auto` délégué au provider ; les prompts LLM disent « détecte la langue » (correct avec les LLM récents, approximatif avec Google). +4. **Cohérence document** : chaque segment est traduit sans contexte document (pas de contexte titre/section). Les LLM supporteraient un batch contextuel. + +--- + +## 5. Architecture — points fragiles + +- **Jobs non persistés** : `asyncio.create_task` (`translate_routes.py:959`) — un redémarrage du process perd tous les jobs en cours (le statut Redis survit mais rien ne reprend). Pour un SaaS payant, prévoir une queue (Celery/Arq/RQ ou table jobs + worker) avec reprise. +- **Cache mémoire seulement** (LRU 5 000, perdu au redémarrage). Le cache Redis avec clés sha256 + TTL 24h **existe** (`services/translation_cache.py`) mais n'est branché nulle part en production — c'est une mémoire de traduction (TM) gratuite à activer. + +--- + +## 6. Benchmark & recommandations produit (recherche web, août 2026) + +### Concurrents et standards du marché +| Acteur | Positionnement | Enseignement | +|---|---|---| +| DeepL | Référence qualité ; documents pdf/docx/pptx/xlsx/idml ; facturation API **min 50 000 chars/document** ; glossaires natives, formality, variantes pt-BR/pt-PT, fr-CA | Le minimum 50K chars est un pain point : un pipeline LLM peut être moins cher. Refuse les PDF scannés. | +| Google Cloud Translation Advanced | **0,08 $/page**, PDF natifs et scannés (OCR intégré, 1 000 pages) | Alternative crédible au scraping gratuit pour le paid tier | +| Azure Translator | 15 $/M chars, batch documents, refuse PDF scannés | | +| Immersive Translate / BabelDOC | 20M+ utilisateurs, PDF bilingue, OCR | La sortie bilingue est très demandée | +| PDFMathTranslate (35K★) / BabelDOC (open source) | Préservation layout PDF, formules, multi-moteurs | À étudier pour améliorer `pdf_translator` | +| Pairaphrase/Redokun/Smartcat | TM + glossaires + relecture humaine, 285 $+/mois | Le segment pro attend TM/glossaires | +| DocTranslator, oTranslator | 14,99 $/mois ou ~0,006-0,06 $/page | Prix marché prosumer : 8-15 $/mois | + +État de l'art modèles (WMT25) : **GPT-4.1 et Gemini-2.5-Pro mènent** la traduction IA ; DeepL next-gen « stable mais mid-tier ». Qualité : MetricX-24/25, xCOMET (QE), RUBRIC-MQM (LLM-judge) — aucun concurrent grand public n'affiche de scores de confiance par segment : **différenciateur ouvert**. + +### Recommandations priorisées +1. **P0 — Corriger les 2 bugs P1** (chinois, L2) : quelques lignes, impact utilisateur direct. +2. **P0 — Activer le cache Redis existant comme TM** (déjà codé) : baisse de coût + cohérence terminologique entre documents. +3. **P1 — Paragraph-level translation avec placeholders** pour docx/pptx : plus grand gain qualité perçue. +4. **P1 — PDF scannés via OCR** : ✅ **implémenté (Mistral OCR, voir §0)**. Reste : brancher Mistral OCR (~1 $/1 000 pages) — il suffit de définir `MISTRAL_API_KEY`. +5. **P1 — Glossaire :** fusionner avec le prompt par défaut ; supporter les glossaires natifs DeepL ; avertir si provider incompatible. +6. **P2 — Sortie bilingue** (docx avec colonnes/annotations, PDF duo) : forte demande, faible coût. +7. **P2 — Activer L0/L1 en production** après fix (coût ~0,0003 $/job) et exposer un « score de confiance » par document. +8. **P2 — Formalité & variantes régionales** (fr-CA, pt-BR/PT) via instructions prompt (LLM) — table stakes 2026. +9. **P3 — Queue persistante** pour les jobs ; remplacer le moteur par défaut du paid tier par DeepL API ou Google Cloud Advanced (0,08 $/page) plutôt que le scraping gratuit. +10. **P3 — Nettoyage :** supprimer le code mort listé §3, documenter l'alias « zai »→xAI. + +--- + +## 7. Annexes + +### A. Résultats des tests réels +Voir §2 — script `temp/audit/audit_functional.py`, sorties dans `temp/audit/`. + +### B. Suite pytest +- `tests/test_translators/test_word_translator.py + test_excel_translator.py + test_pptx_translator.py` : **106 passed en 3,68 s** ✅ +- `tests/test_translators/test_b3_5_pdf_smart_fit.py` : **bloque indéfiniment** sur `test_tier_3_4_limit_to_two_shrink_steps` (crash natif libmagic, cf. bug P2 ci-dessus) — reproduit isolément ; dumps faulthandler « Windows fatal exception: access violation » dans `magic/compat.py:189`. +- Recommandations : corriger l'import `magic`, ajouter `pytest-timeout`, et noter que `pytest.ini` active la couverture par défaut (`addopts --cov=...`) ce qui rend les runs complets très lents. + +### C. Sources principales (benchmark) +- DeepL document translation & API : deepl.com/en/features/document-translation · developers.deepl.com (document upload, glossaries, formality, variants) +- Google Cloud Translation (documents, 0,08 $/page, OCR) : cloud.google.com/translate/docs/advanced/translate-documents +- Azure Translator document translation : learn.microsoft.com/azure/ai-services/translator/document-translation/ +- WMT25 (GPT-4.1/Gemini 2.5 Pro en tête) : slator.com/wmt25-preliminary-results-gemini-2-5-pro-gpt-4-1-lead-ai-translation/ +- MetricX-24/25 (QE) : github.com/google-research/metricx · RUBRIC-MQM : aclanthology.org/2025.acl-industry.12/ +- PDFMathTranslate : github.com/PDFMathTranslate/PDFMathTranslate · BabelDOC : github.com/funstory-ai/BabelDOC +- Mistral OCR (1 $/1 000 pages) : mistral.ai/news/mistral-ocr/ · PaddleOCR : github.com/PaddlePaddle/PaddleOCR +- Immersive Translate (bilingue, PDF Pro) : immersivetranslate.com/pricing/ +- Pairaphrase (pricing entreprise) : pairaphrase.com · Redokun : redokun.com/pricing diff --git a/docs/SESSION_2026-08-29_FEATURES.md b/docs/SESSION_2026-08-29_FEATURES.md new file mode 100644 index 0000000..09cc6f8 --- /dev/null +++ b/docs/SESSION_2026-08-29_FEATURES.md @@ -0,0 +1,37 @@ +# Session du 2026-08-29 (3ᵉ passe) — « Fais tout » : quick wins + items médiums livrés + +> Décision produit de la session : **aucune intégration DeepL** (refus explicite). Formalité et variantes régionales passent exclusivement par les prompts LLM. + +## Livré dans cette session (tout testé — 23 nouveaux tests + 1 mis à jour) + +| # | Fonctionnalité | Implémentation | Tests | +|---|---|---|---| +| 1 | **Formalité (formel/informel)** | Nouveau paramètre `formality` sur `POST /api/v1/translate` → directive `TONE:` ajoutée au prompt de tous les moteurs LLM (openai/deepseek/minimax/legacy) via `build_full_prompt` | 3 | +| 2 | **Variantes régionales automatiques** | `target_lang` régional (pt-BR, fr-CA, zh-CN…) → directive `REGIONAL VARIANT: write in ` dans le prompt | 2 | +| 3 | **Bug corrigé : prompt personnalisé qui remplaçait les instructions** | Les 3 providers LLM incluent TOUJOURS le prompt de base ; glossaire/ton/contexte s'y ajoutent (`ADDITIONAL CONTEXT AND INSTRUCTIONS`) — avant, un glossaire seul produisait un prompt sans instruction de traduction | 1 (+ openai) | +| 4 | **Mémoire de traduction (TM) activée** | `services/translation_tm.py` branche le cache Redis existant (`translation_cache.py`, TTL 24 h, fallback LRU sans Redis) dans Word/Excel/PPTX. **Périmètre par utilisateur** (jamais de partage inter-clients) + hash du contexte (glossaire/ton) pour invalidation. Les traductions identiques à la source ne sont jamais stockées (anti-poison). Repli silencieux si le provider échoue | 5 | +| 5 | **Sortie bilingue (docx)** | Paramètre `output_mode=bilingual` : chaque paragraphe traduit est précédé de sa source (gris, italique, 9 pt) — `translators/bilingual.py`. Repli propre si structure divergente | 2 | +| 6 | **Rapport QA + score de confiance** | `services/quality/qa_report.py` : fidélité des nombres (séparateurs décimaux normalisés), ratio non-traduit, **score 0-100** exposé dans `GET /api/v1/translations/{id}` (champ `quality`) et le statut de complétion. Jamais bloquant | 5 | +| 7 | **Batch JSON pour LLM** | `openai_provider.translate_batch` envoie un chunk entier en **1 requête** (liste JSON numérotée) au lieu de 15 requêtes isolées — ~15× moins d'appels, cohérence contextuelle entre segments voisins. Repli automatique par item si la réponse est non conforme | 3 | +| 8 | **Qualité L0 par défaut ON** | `QUALITY_L0_ENABLED=true` par défaut (observabilité pure, ne bloque jamais) | — | +| 9 | **Polices CJK/arabe** | Word : hints `w:eastAsia` (SimSun/Yu Mincho/Batang) et `w:cs` sur les runs ; PPTX : `` typeface ; PDF : chemins Noto CJK (Linux) + simsun/msgothic (Windows) ajoutés | 2 | + +**Exemples d'appel :** +```bash +# Ton formel + portugais brésilien +curl -F file=@doc.docx -F target_lang=pt-BR -F formality=formal .../api/v1/translate + +# Sortie bilingue +curl -F file=@doc.docx -F target_lang=en -F output_mode=bilingual .../api/v1/translate +``` + +## Vérifications +- Suite complète : voir ligne finale ci-dessous (6 tests réseau Google désélectionnés — endpoint gratuit momentanément bloqué depuis cette machine, sans lien avec le code). +- `tsc --noEmit` OK sur les deux frontends. + +## Non livré (justifié) +- **Éditeur de relecture côte à côte** : nécessite la persistance des segments par job (schéma BDD + UI d'édition) — chantier à part entière, il ouvre la voie au XLIFF. +- **Espaces de travail équipes (rôles, glossaires partagés)** : migrations + facturation multi-sièges. +- **XLIFF export/import** : dépend de la persistance des segments (même chantier que l'éditeur). +- **IDML/DITA** : nouveaux parseurs de formats — à chiffrer séparément. +- **DeepL natif (glossaires/formalité API)** : refusé par le propriétaire — la formalité LLM couvre le besoin sans DeepL. diff --git a/docs/marketing/PLAN-ALIGNEMENT-CODE.md b/docs/marketing/PLAN-ALIGNEMENT-CODE.md new file mode 100644 index 0000000..1ed2172 --- /dev/null +++ b/docs/marketing/PLAN-ALIGNEMENT-CODE.md @@ -0,0 +1,103 @@ +# Plan d'alignement Marketing ↔ Code — wordly.art + +> Analyse effectuée le 2026-08-29, croisée ligne à ligne avec le code backend, la landing et les docs de lancement. +> Principe : **le lancement (Product Hunt / Show HN) attire des relecteurs techniques** — chaque promesse non tenue y sera testée publiquement. Ce plan sépare ce qu'il faut corriger **dans les textes** (rapide) de ce qu'il faut corriger **dans le code** (pour tenir les promesses). +> +> **Statut au 2026-08-29 : vagues A et B appliquées et testées (442 tests backend passent, `tsc --noEmit` landing OK). Restent les items P2 de la vague C.** + +--- + +## 1. Ce qui est déjà conforme ✅ + +| Affirmation marketing | Vérification code | Statut | +|---|---|---| +| Prix 0/9/19/49 €, annuel −20 % (7,20/15,20/39,20) | `models/subscription.py:39-155` (86,40/182,40/470,40 €/an) | ✅ exact | +| Quotas 2/50/200/1 000 docs, pages 10/50/200/500, fichiers 5/10/25/50 Mo | idem | ✅ exact | +| API Business 10 000 appels/mois, 5 sièges | `api_calls_per_month: 10_000`, `team_seats: 5` | ✅ exact | +| Crédits 50/5 € · 100/9 € · 250/20 € · 500/35 € · 1 000/60 € (0,06–0,10 €) | `CREDIT_PACKAGES` (`models/subscription.py:316`) | ✅ exact | +| Landing : sections pricing/FAQ/preuve/waitlist + Vercel Analytics + waitlist endpoint + compteur | `app/page.tsx`, `components/*`, `routes/waitlist_routes.py` | ✅ en place | +| Événements analytics (`translation_started`, `pricing_cta_click`, `waitlist_joined`, `waitlist_error`) | présents dans les 3 composants | ✅ branchés | +| Footer sans « SOC 2 » | `app/page.tsx` — mention absente | ✅ corrigé | +| Blog Docker : « CORS `*` refuse de démarrer en prod » | `main.py:379-386` (`sys.exit`) | ✅ vrai | +| « DeepL moteur des plans payants » | Starter+ : `providers: ["google","deepl",…]` | ✅ exact | +| Blog 1 : « à partir de 0,06 €/page » | cohérent avec les crédits (1 page = 1 crédit de base) | ✅ | + +--- + +## 2. Écarts détectés — promesses non tenues par le code + +### E1. 🔴 « PDF is on the roadmap » (Show HN + Product Hunt) — FAUX, et ça cache une feature +- Show HN : *« (PDF is the obvious next one.) »* · Product Hunt : *« PDF is on the roadmap »*. +- **Réel : le PDF est déjà supporté** (`.pdf` dans `SUPPORTED_EXTENSIONS`, mode `layout`/`text_only`, pdf2docx fallback) **et depuis le 2026-08-29 les PDF scannés passent par OCR Mistral** (`services/mistral_ocr.py`) — ce que DeepL et Azure refusent. +- **C'est l'inverse : c'est un argument de vente majeur à ajouter.** Telle quelle, la phrase invite les relecteurs HN à trouver une fonctionnalité qui existe déjà dans le dashboard. + +### E2. 🔴 « Files auto-deleted after 60 min » (landing, FAQ, Show HN, PH, Reddit, X, blogs 1/3) — inexact +- Réel (`config.py`) : uploads **30 min**, résultats **120 min**, TTL générique 60. +- Deux options : corriger le texte (« uploads deleted after 30 min, results within 2 h ») **ou** passer `OUTPUT_FILE_TTL_MINUTES` à 60. Recommandation : garder 120 min (l'utilisateur doit pouvoir re-télécharger) et corriger les textes. + +### E3. 🔴 « Encrypted at rest » (FAQ §sécurité, X-thread, blog 3 §données) — non implémenté +- Aucun chiffrement des fichiers au repos dans le code (uploads/outputs en clair). TLS : dépend du reverse-proxy (vrai en prod nginx+HSTS). +- **À retirer des textes** tant que non implémenté (ou implémenter, cf. §3-C5). + +### E4. 🟠 « 60+ langues » (landing stats, PH, X-thread) — l'app en expose 35 +- Réel : `/api/v1/languages` = **35 langues** ; le sélecteur UI = 35. (Le validateur accepte ~100 codes, mais invisibles pour l'utilisateur.) +- Options : (a) corriger le chiffre en « 35 », (b) **exposer les ~60-100 langues déjà validées** côté API/UI — recommandé, c'est un travail faible et les LLM couvrent ces langues. + +### E5. 🟠 « 7 moteurs » avec Minimax et DeepSeek cités (FAQ, Show HN) — incohérent avec les plans +- Réel (`models/subscription.py`) : Business = `google, google_cloud, deepl, openrouter, openrouter_premium, openai, zai` — **ni Minimax ni DeepSeek direct** (DeepSeek n'est que le modèle IA « essentiel » interne). +- Harmoniser : retirer Minimax/DeepSeek des listes, **ou** les ajouter au plan Business (l'endpoint `/providers/available` les expose déjà à l'UI !). + +### E6. 🟡 « 100 % formatting preserved » (landing, comparatif, x-thread) — survendu +- Cas limites réels : PDF scannés (sortie re-mise en page, pas d'« in place »), placeholders `[translation overflow]` sur débordements, texte dans images non traduit sans l'option vision. +- Recommandation : garder la promesse forte mais crédible (« formatting preserved — formulas, merges, styles ») + publier une page « limites connues ». Sur HN, un contre-exemple suffira à casser le « 100 % ». + +### E7. 🟠 Blog Docker (auto-hébergement) : 4 erreurs factuelles +1. `GOOGLE_TRANSLATE_API_KEY=...` : **cette variable n'existe pas** — le moteur Google gratuit n'a pas de clé ; le moteur payant = `GOOGLE_CLOUD_API_KEY`. Remplacer par un exempilaire réel du `.env.example`. +2. `pg_dump -U wordly wordly` : les défauts du compose sont **`translate` / `translate_db`**. +3. « L'API sur :8000 » : le compose publie **8001:8000** → c'est `:8001` côté hôte. +4. `git clone https://gitea.parsanet.org/...` : repo privé → rendre public ou générer (`github.com//office-translator`). + +### E8. 🟠 Le backend n'applique PAS la grille « moteurs par plan » (fuite de revenus) +- Dans `translate_document_v1`, seul `google_cloud` est rétrogradé pour les non-Pro. **Un Free peut envoyer `provider=openai` ou `openrouter_premium`** et consommer les moteurs vendus 49 €/mois. La grille existe pourtant dans `PLANS[plan]["providers"]`. +- Idem : **`translate_images` (vision) n'est pas gated**, alors que la FAQ dit « Pro and Business plans ». + +--- + +## 3. Plan d'action + +### Vague A — Avant lancement (textes, ~½ journée) — P0 ✅ APPLIQUÉE +| # | Action | Fichiers | Statut | +|---|---|---|---| +| A1 | Remplacer « PDF on the roadmap » par la feature : « PDF supported — incl. scanned PDFs via OCR » | `launch/show-hn.md`, `launch/product-hunt.md` | ✅ | +| A2 | Corriger la rétention : « uploads deleted after 30 min, results auto-deleted within 2 h » | landing `social-proof-section.tsx`, `faq-section.tsx`, `show-hn.md`, `product-hunt.md`, `reddit-posts.md`, `x-thread.md`, `blog/1`, `blog/3`, `MARKETING_PLAN.md` | ✅ | +| A3 | Supprimer « encrypted at rest » / « at rest » | `faq-section.tsx`, `x-thread.md`, `blog/3` | ✅ | +| A4 | Blog Docker : clé `GOOGLE_CLOUD_API_KEY`, `pg_dump` translate/translate_db, port hôte 8001, URL de dépôt générique, + mention MISTRAL_API_KEY | `blog/5-auto-heberger-traduction-docker.md` | ✅ | +| A5 | Ajouter PDF (+ OCR scannés) aux formats annoncés : hero, badges `.pdf`, drag & drop + `accept` PDF, FAQ formats | `hero-section.tsx`, `translation-card.tsx`, `faq-section.tsx` | ✅ | + +### Vague B — Semaine 1 (code, ~2-3 j) — P1 ✅ APPLIQUÉE +| # | Action | Détail | Statut | +|---|---|---|---| +| B1 | **Gating moteurs par plan** : `translate_document_v1` rejette (403 `PRO_FEATURE_REQUIRED`) tout `provider` hors `PLANS[tier]["providers"]` ; helpers testés (`tests/test_plan_gating.py`) ; `/providers/available` filtré par plan (l'UI ne propose plus un moteur refusé) | ferme la fuite de revenus E8 | ✅ | +| B2 | Gater `translate_images` aux plans Pro+ | la FAQ redevient exacte | ✅ | +| B3 | Langues : `/api/v1/languages` expose désormais **107 langues nommées** (35 populaires d'abord, puis ordre alphabétique ; `LANGUAGE_NAMES` complété) → la promesse « 60+ » est tenue | E4 résolu par le code | ✅ | +| B4 | Moteurs harmonisés : Minimax/DeepSeek retirés des listes marketing (DeepSeek présenté « via OpenRouter ») ; cohérent avec le filtrage du catalogue | résout E5 | ✅ | + +### Vague C — Avant le scale — P2 +| # | Action | Détail | +|---|---|---| +| C1 | Remplacer « 100 % » par une promesse démontrable + page « limites connues » (PDF scannés re-mis en page, overflow, images) | E6 | +| C2 | Si l'argument « chiffré au repos » est voulu : chiffrer `uploads/`+`outputs/` (clé env `FILES_ENCRYPTION_KEY`, chiffrement AES au write/read) alors seulement réintroduire la mention | E3 | +| C3 | Mettre à jour `MARKETING_PLAN.md` : ajouter PDF + OCR scannés aux différenciateurs (vs DeepL/Azure qui refusent les scannés), mentionner webhooks & API keys | valorise l'existant | +| C4 | Décider TTL outputs : si la promesse « 60 min » est gardée, passer `OUTPUT_FILE_TTL_MINUTES=60` au lieu de changer 9 textes | alternative à A2 | +| C5 | NPS « enquête post-traduction » (source KPI dans MARKETING_PLAN §6) : non implémenté — retirer la ligne ou créer le mini-formulaire post-download | KPIs honnêtes | + +### Decision needed (propriétaire) — ✅ TRANCHÉES LE 2026-08-29 +- **Rétention** : textes corrigés (A2) — les TTL code restent 30 min / 120 min. +- **Langues** : 107 exposées via `/languages` (B3) — la promesse « 60+ » est tenue. +- **Minimax/DeepSeek** : retirés du discours commercial (DeepSeek mentionné « via OpenRouter ») ; plans inchangés. + +--- + +## 4. Résumé + +Le socle commercial du document marketing est **fiable** (prix, quotas, crédits, API, landing, waitlist, analytics : tout correspond au code). Les écarts se concentrent sur **6 chiffres/affirmations répétées** (60 min, 60+ langues, 7 moteurs, 100 %, chiffrement au repos, « PDF à venir ») et **2 trous d'application côté code** (gating moteurs et vision par plan). Les vagues A (textes) et B (code) suffisent pour un lancement PH/HN sans Vulnerabilité factuelle. diff --git a/docs/marketing/blog/1-traduire-excel-sans-perdre-la-mise-en-forme.md b/docs/marketing/blog/1-traduire-excel-sans-perdre-la-mise-en-forme.md new file mode 100644 index 0000000..8880e48 --- /dev/null +++ b/docs/marketing/blog/1-traduire-excel-sans-perdre-la-mise-en-forme.md @@ -0,0 +1,89 @@ +# Comment traduire un fichier Excel sans perdre la mise en forme + +> Article pilier SEO — objectif : 1 500+ mots au moment de la publication (ce brouillon est complet, à enrichir avec 2 captures d'écran réelles et un exemple chiffré avant publication). + +## Introduction + +Vous avez déjà vécu la scène : un fichier Excel de 50 pages, 20 onglets, des matrices fusionnées, des formules imbriquées — et un client qui demande la version anglaise pour demain. + +Les solutions « classiques » ne font pas ce que vous pensez. Nous les avons toutes testées. Voici ce qu'elles font réellement à votre fichier, et comment le traduire sans rien casser. + +## Ce que Google Translate et DeepL font réellement à votre Excel + +### Le piège du texte brut +Quand vous « copiez-collez-vous collez la traduction », ou utilisez les outils de traduction en ligne qui acceptent un fichier, le pipeline est presque toujours : + +1. **Extraction** : le fichier est aplati en texte brut (ou en CSV) +2. **Traduction** : le texte est traduit +3. **Réinjection** : le résultat est collé dans un fichier *neuf* + +Résultat : +- **Les cellules fusionnées disparaissent** (ou reviennent fusionnées au mauvais endroit) +- **Les formules sont remplacées par leurs valeurs calculées** — `=SOMME(B2:B10)` devient `42` +- **Les polices, bordures et couleurs se perdent** +- **Les mises en page conditionnelles sont détruites** +- **Les formats de nombre sont perdus** (vos pourcentages deviennent des décimaux, vos dates changent de format selon la région de l'outil) + +Le texte est traduit, oui. Mais le fichier n'est plus *votre* fichier. + +### Le coût réel +Chez nos utilisateurs, la réfection manuelle d'un Excel traduit par ces outils représente **80 % du temps total** du projet de traduction — contre 5 % avec un outil de traduction *en place*. + +## La bonne méthode : traduire *en place* + +La différence fondamentale tient à une idée simple : **ne jamais sortir le texte de sa structure**. + +Un fichier .xlsx est, sous le capot, une archive ZIP contenant du XML. Les cellules, les fusions, les formules et les styles sont des éléments XML distincts. Un bon moteur de traduction de documents : + +1. **Parse** la structure native (feuilles, cellules, fusions, formules, styles) +2. **Extrait uniquement** le contenu traduisable (chaînes de caractères), en mémorisant leur position et leur style +3. **Traduit** ces chaînes (le moteur de votre choix) +4. **Réinjecte** les chaînes traduites dans *la même* structure XML +5. **Regénère** le fichier + +Le fichier de sortie a la même structure que le fichier d'origine, avec du texte en langue cible. C'est exactement ce que fait Office Translator pour .xlsx, .docx et .pptx. + +## Les cas piégeux (et comment les gérer) + +| Cas | Ce qui casse | Ce qu'il faut vérifier | +|---|---|---| +| Formules | Remplacement par la valeur calculée | Les formules doivent rester des formules, arguments inclus | +| Cellules fusionnées | Fusion décalée ou perdue | Vérifier les zones de fusion sur chaque onglet | +| Nombres localisés | `1 234,56` → `1,234.56` (ou l'inverse) | Les formats de nombre doivent suivre la langue cible | +| Dates | `31/12/2026` → `2026-12-31` (ou casse totale) | Format de date aligné sur la localisation | +| Textes dans les images | Ignorés par la plupart des outils | Un modèle vision est nécessaire (disponible sur les plans payants) | +| Commentaires et annotations | Perdus à l'extraction | Vérifier la présence des commentaires en sortie | + +**Règle d'or** : après chaque traduction, faites un contrôle *structurel* (même nombre d'onglets, mêmes fusions, mêmes formules) avant le contrôle sémantique. Un outil qui traduit en place vous fait gagner ce contrôle : vous vérifiez le texte, pas la mise en page. + +## Comparatif des approches (2026) + +| Approche | Format préservé | Formules | Coût | Délai | +|---|---|---|---|---| +| Traducteur humain | Oui (réintégration manuelle) | Oui | 50–100× plus cher | Jours–semaines | +| Google Translate (copier-coller) | Non | Non | Gratuit | Minutes | +| Outil one-shot (DocTranslator et sim.) | Partiel | Non | Payant, variable | Minutes | +| **Traduction en place (Office Translator)** | **Oui, 100 %** | **Oui** | **À partir de 0,06 €/page** | **Minutes** | + +## Comment choisir un outil de traduction de documents + +Avant de payer, testez sur *votre* fichier le plus sale (pas un exemple propre) et vérifiez : + +1. **Structure** : mêmes onglets, mêmes fusions, mêmes colonnes +2. **Formules** : ouvrez la barre de formule, pas seulement la valeur affichée +3. **Styles** : polices, bordures, couleurs de cellules +4. **Formats** : nombres, dates, pourcentages, devise +5. **Confidentialité** : où vont vos fichiers ? Combien de temps sont-ils conservés ? (Chez nous : suppression automatique — envois sous 30 minutes, résultats sous 2 heures —, zéro rétention, jamais utilisé pour l'entraînement de modèles.) + +## Conclusion + +Traduire un Excel sans perdre la mise en forme, c'est possible — mais seulement avec un outil qui travaille *dans* la structure du fichier, pas *autour*. + +Testez le plan gratuit (2 documents/mois, sans carte bancaire) sur votre fichier le plus complexe, et comparez. C'est le meilleur moyen de voir la différence. + +--- +**Métadonnées SEO** +- Title : « Traduire un Excel sans perdre la mise en forme : guide 2026 » +- Meta description : « Formules, cellules fusionnées, styles : ce qui casse vraiment vos Excel traduits, et comment les traduire en place sans rien perdre. Test gratuit, sans carte. » +- Mot-clés principaux : traduire excel, translation excel, traduire document excel, mise en forme excel +- URLs cibles : /blog/traduire-excel-mise-en-forme diff --git a/docs/marketing/blog/2-comparatif-outils-traduction-2026.md b/docs/marketing/blog/2-comparatif-outils-traduction-2026.md new file mode 100644 index 0000000..9fcd284 --- /dev/null +++ b/docs/marketing/blog/2-comparatif-outils-traduction-2026.md @@ -0,0 +1,74 @@ +# Les 5 meilleurs outils de traduction de documents comparés (2026) + +> Article comparatif SEO — brouillon complet. À compléter avec captures réelles de chaque outil (section « verdict ») avant publication. + +## Introduction + +En 2026, traduire un document de bureau est devenu un marché encombré : traducteurs humains, assistants IA, outils one-shot, plateformes TMS d'entreprise. Mais très peu d'entre eux résolvent le vrai problème : **traduire sans casser la mise en page**. + +Nous avons comparé les 5 approches que vous allez réellement croiser, avec leurs forces, leurs faiblesses et le cas d'usage où chacune gagne. (Nous en faisons partie — ce comparatif est donc à lire avec cette lunette, et nous avons pris soin de ne pas nous déclarer vainqueur par défaut.) + +## Les 5 catégories + +### 1. Traducteurs humains (agences) +- **Force** : qualité sémantique imbattable, gestion des nuances, relecture +- **Faiblesse** : coût (50–100× une machine) et délai (jours à semaines) +- **Gagne quand** : contenus à forte valeur, juridiques, marketing, tout ce qui ne supporte pas l'erreur +- **À savoir** : un humain traduit *le texte*. La réintégration dans le fichier (Excel, PPT) reste souvent une étape manuelle distincte, facturée à part + +### 2. Google Translate (docs) +- **Force** : gratuit, instantané, 100+ langues, connu de tous +- **Faiblesse** : aplatit le document en texte brut. Formules, fusions, styles, animations : perdus. Qualité « correcte » mais non professionnelle +- **Gagne quand** : traduction d'exploration, comprendre un document, pas le livrer +- **À savoir** : c'est l'étalon du « gratuit qui casse le format » — le contrepied de notre positionnement + +### 3. DeepL (documents) +- **Force** : qualité de traduction supérieure à Google pour les textes professionnels +- **Faiblesse** : formatage limité sur les documents complexes ; moteur unique (vous ne choisissez pas l'IA) ; prix plus élevés +- **Gagne quand** : textes longs, qualité prioritaire, budget disponible +- **À savoir** : DeepL est l'un de *nos* moteurs — sur les plans payants, vous pouvez le choisir par document, au même titre que d'autres + +### 4. Plateformes TMS d'entreprise (Smartling, Smartcat, Transifex) +- **Force** : gestion de flux, glossaires, collaboration, intégrations +- **Faiblesse** : lourdes, chères, surdimensionnées pour une PME ; orientées localisation logicielle/i18n plus que documents bureautiques +- **Gagne quand** : grande entreprise, volumes massifs, processus multi-équipes +- **À savoir** : Transifex est notamment très fort sur la localisation de logiciels, moins sur les fichiers Word/Excel/PowerPoint + +### 5. Traduction en place multi-moteurs (Office Translator) +- **Force** : 100 % de préservation du format (fusions, formules, styles, slides), choix parmi 7 moteurs, glossaires techniques, API +- **Faiblesse** : plus jeune que les TMS ; l'écosystème est encore en construction +- **Gagne quand** : PME/ agences qui veulent du « prêt à livrer » sans réfection manuelle +- **À savoir** : c'est nous ; le plan Free (2 docs/mois) permet de tester sans carte + +## Tableau récapitulatif + +| Critère | Humain | Google | DeepL | TMS entreprise | **Office Translator** | +|---|---|---|---|---|---| +| Format préservé | Manuel | Non | Partiel | Variable | **Oui (100 %)** | +| Formules Excel | Oui | Non | Non | Variable | **Oui** | +| Choix du moteur | n/a | 1 | 1 | 1–2 | **7** | +| Glossaires | Oui | Non | Oui (payant) | Oui | **Oui** | +| API | Non | Oui | Oui | Oui | **Oui (Business)** | +| Coût relatif | 50–100× | 0× | 5–10× | 20–50× | **1× (réf.)** | +| Délai | Jours | Secondes | Minutes | Jours | **Minutes** | +| Confidentialité | Contractuel | Variable | Variable | Contractuel | **Zéro rétention, TTL 60 min** | + +## Verdict par profil + +- **Freelance / étudiant** → plan Free, puis Starter si volume +- **PME internationale** → Pro (multi-moteurs + IA + glossaires) +- **Agence de traduction** → Business (API + 5 sièges) pour un premier passage machine, relecture humaine +- **Grande entreprise / volume massif** → comparez avec un TMS ; l'API Business peut suffire, ou Enterprise sur mesure + +## Conclusion + +Il n'y a pas de « meilleur outil » universel — il y a le bon outil pour votre cas d'usage. La seule question qui ne devrait jamais se poser : **votre document ressort-il intact ?** C'est le critère que la plupart des outils gratuits et one-shot échouent, et celui sur lequel nous avons construit tout le produit. + +Testez le plan gratuit sur votre fichier le plus sale et jugez. + +--- +**Métadonnées SEO** +- Title : « Outils de traduction de documents 2026 : comparatif complet » +- Meta description : « Humain, Google, DeepL, TMS ou traduction en place : 5 approches comparées sur format, prix, délai et confidentialité. Trouvez celle qui vous correspond. » +- Mots-clés : comparatif traduction documents, meilleur outil traduction, deepl vs google, traduire word excel powerpoint +- URL : /blog/comparatif-outils-traduction-document-2026 diff --git a/docs/marketing/blog/3-traduction-professionnelle-pme-guide.md b/docs/marketing/blog/3-traduction-professionnelle-pme-guide.md new file mode 100644 index 0000000..56e80e4 --- /dev/null +++ b/docs/marketing/blog/3-traduction-professionnelle-pme-guide.md @@ -0,0 +1,83 @@ +# Traduction professionnelle de documents : guide complet pour les PME + +> Article guide SEO — brouillon complet. À illustrer avec 2 captures d'écran réelles (glossaire + dashboard) avant publication. + +## Introduction + +Une PME qui exporte, qui a des clients étrangers ou qui s'internationalise produit des dizaines de documents par mois : fiches techniques, matrices tarifaires, contrats, présentations commerciaux, manuels utilisateurs. Traduire tout ça « à la main » coûte une fortune et des semaines. Le guide complet pour y voir clair — et choisir sans se faire avoir. + +## 1. Évaluez votre volume réel + +Avant de choisir quoi que ce soit, mesurez : + +- **Documents/mois** par type (Excel, Word, PowerPoint) +- **Pages/moyenne par document** (c'est la page, pas le document, qui se paie en traduction humaine) +- **Fréquence des mises à jour** (un tarif qui change chaque mois ne se traduit pas une fois) +- **Langues cibles** (1 langue = simple ; 5 langues = le coût multiplie par 5) + +**Règle rapide** : si vous dépassez ~50 pages/mois, la traduction humaine pure devient votre poste de dépense n°1 en localisation. + +## 2. Les 3 stratégies + +### Stratégie A — 100 % humaine +- Qualité maximale, zéro risque +- Coût : comptez 0,08–0,25 € le mot selon le domaine (légal et technique en haut de fourchette) +- Délai : 2–5 jours ouvrés par lot +- **Pour qui** : contenu à forte valeur, marchés réglementés, image de marque + +### Stratégie B — 100 % machine +- Coût quasi nul, délai en minutes +- Risque : formats cassés (si l'outil est mauvais), terminologie incohérente, erreurs sémantiques +- **Pour qui** : contenu interne, exploration, brouillons + +### Stratégie C — Hybride (la plus rentable pour la majorité des PME) +1. **Premier passage machine** avec un outil qui préserve le format (fusions, formules, styles intacts) +2. **Terminologie verrouillée** via un glossaire (vos termes, pas ceux du moteur) +3. **Relecture humaine ciblée** sur les documents clients à forte valeur uniquement + +Résultat typique : **70–90 % du délai humain en moins, pour 10–20 % du coût** — en gardant la qualité sur ce qui compte. + +## 3. Les critères de choix d'un outil + +| Critère | Pourquoi c'est décisif | +|---|---| +| **Préservation du format** | Le vrai coût caché est la réfection manuelle. Testez sur VOTRE fichier le plus complexe | +| **Multi-moteurs** | Un moteur unique = un point de défaillance et un plafond de qualité. 7 moteurs au choix = la bonne réponse au bon prix | +| **Glossaires** | Sans glossaire, « unité de froid » devient « cold unit » un jour et « chiller » le lendemain. Inacceptable en B2B | +| **Coût par page** | Comparez toujours à la page, jamais au document (un document fait 3 pages ou 300) | +| **Confidentialité** | Où vont vos fichiers ? Durée de conservation ? Utilisation pour l'entraînement ? (Chez nous : suppression automatique — envois 30 min, résultats ≤ 2 h —, zéro rétention, jamais d'entraînement sur vos données) | +| **API** | Si vous voulez automatiser (CRM, e-signature, ERP), l'API n'est pas optionnelle | + +## 4. Mettre en place le glossaire (l'étape que tout le monde saute) + +1. **Collectez** vos 50–200 termes récurrents (produits, acronymes, termes légaux, noms propres) +2. **Validez** la traduction officielle de chaque terme avec votre équipe (une seule source de vérité) +3. **Chargez** le glossaire dans l'outil et appliquez-le à chaque document +4. **Itérez** : chaque nouvelle ambiguïté trouvée en relecture devient une entrée de glossaire + +Après 2 mois de glossaire mature, la relecture humaine se concentre sur le sens, pas sur la terminologie. C'est là que le ROI explose. + +## 5. Confiance et sécurité + +- **Données** : chiffrement en transit (TLS), suppression automatique des fichiers (30 min pour les envois, 2 h max pour les résultats, chez nous) +- **Conformité** : vérifiez les engagements de l'éditeur (RGPD pour les clients UE) +- **Sauvegarde de votre travail** : gardez toujours le fichier source en version originale — aucun outil ne devrait être un point de perte unique + +## 6. Ce que ça coûte vraiment + +Repère : une page de document bureautique traduite et *réintégrée* par un humain coûte 5–20 €. En hybride (machine + relecture ciblée), comptez 0,50–1,50 € la page. Sur 500 pages/mois, l'écart est de **plusieurs milliers d'euros par mois**. + +C'est la ligne à regarder avant de signer quoi que ce soit. + +## Conclusion + +La traduction professionnelle de documents pour une PME ne se joue pas sur « machine ou humain » — elle se joue sur **format préservé + terminologie verrouillée + relecture ciblée**. Choisissez l'outil qui livre les deux premières, et gardez les humains sur la troisième. + +Le plan Free (2 documents/mois, sans carte) vous permet de tester la méthode sur vos vrais fichiers avant de décider. + +--- +**Métadonnées SEO** +- Title : « Traduction professionnelle de documents : guide PME 2026 » +- Meta description : « Humain, machine ou hybride : le guide complet pour traduire vos documents PME sans casser le format ni le budget. Coûts, glossaires, confidentialité. » +- Mots-clés : traduction documents PME, traduction professionnelle, coût traduction, glossaire traduction +- URL : /blog/traduction-professionnelle-pme-guide diff --git a/docs/marketing/blog/4-pourquoi-deepl-google-cassent-vos-excel.md b/docs/marketing/blog/4-pourquoi-deepl-google-cassent-vos-excel.md new file mode 100644 index 0000000..c74611e --- /dev/null +++ b/docs/marketing/blog/4-pourquoi-deepl-google-cassent-vos-excel.md @@ -0,0 +1,90 @@ +# Pourquoi DeepL et Google Translate « détruisent » vos documents Excel + +> Article problématique SEO — brouillon complet. Ton direct. À illustrer d'un avant/après réel (capture) avant publication. + +## Introduction + +DeepL est excellent. Google Translate est gratuit. Et pourtant, des équipes entières maudissent les deux chaque semaine pour la même raison : **leurs fichiers Excel n'ont plus rien à voir avec l'original une fois traduits**. + +Ce n'est pas un bug. C'est un choix d'architecture. Voici ce qui se passe réellement sous le capot, et ce qu'il faudrait pour faire mieux. + +## Ce que ces outils font *vraiment* d'un fichier Excel + +### Un .xlsx n'est pas une grille, c'est du XML + +Sous le capot, `compta.xlsx` est une archive ZIP contenant : + +- `sheet1.xml` — les cellules, leurs valeurs, leurs styles +- `sharedStrings.xml` — le texte dédupliqué +- `styles.xml` — polices, bordures, formats de nombre +- `workbook.xml` — fusions de cellules, onglets, mises en page + +### Le pipeline « standard » de traduction + +Tous les outils de traduction de documents — y compris les très bons — suivent ce schéma : + +``` +Excel → extraction en texte brut → traduction → réinjection dans un fichier NEUF +``` + +Chaque étape de perte : + +| Étape | Ce qui est perdu | +|---|---| +| Extraction | Formules (remplacées par la valeur), fusions, styles, formats de nombre, commentaires | +| Traduction | Rien — c'est là que ça va bien | +| Réinjection | Tout ce qui n'était pas du texte : le fichier de sortie est une *coquille vide* | + +**Le résultat** : le texte est en anglais, mais votre fichier de comptabilité est devenu un fichier de texte habillé en Excel. Les formules sont mortes, les colonnes ont bougé, les pourcentages sont devenus des décimaux. + +### Pourquoi DeepL ne peut pas faire « mieux » (en l'état) + +DeepL traduit *le texte*, superbement. Mais son API document est pensée pour du texte, pas pour la **structure** d'un fichier Office. L'ingénierie de réintégration (reconstruire le XML d'origine avec les chaînes traduites, en conservant formules et fusions) est exactement le travail qu'un outil *spécialisé* fait — et qu'un traducteur généraliste ne fait pas. + +Google, lui, ne fait même pas l'étape 3 proprement : vous collez, il recolle, et c'est tout. + +## Ce qu'un outil « en place » fait de différent + +La différence tient à une inversion : **ne jamais sortir le texte de sa structure**. + +1. **Parse** le XML natif (cellules + fusions + formules + styles) +2. **Extrait uniquement** les chaînes traduisables, en mémorisant position et style +3. **Traduit** ces chaînes (au choix : DeepL, Google, ou un LLM contextuel) +4. **Réinjecte** dans *le même* XML +5. **Regénère** le fichier + +Même structure, même nombre de cellules, mêmes fusions, mêmes formules — du texte en langue cible. C'est la seule architecture qui donne un fichier « prêt à livrer ». + +## Testez-le vous-même (5 minutes) + +Prenez votre fichier le plus sale — fusions, formules, colonnes formatées : + +1. Traduisez-le avec l'outil que vous utilisez habituellement +2. Ouvrez la **barre de formule** d'une cellule qui en contenait une +3. Vérifiez une fusion de cellules +4. Regardez un pourcentage + +Si vous avez dû « refaire » le fichier, vous venez de payer la traduction *deux fois* : une fois à la machine, une fois à vous. + +## Ce que ça change concrètement + +| | Pipeline standard | Traduction en place | +|---|---|---| +| Formules | Morte (valeur figée) | Intacte | +| Fusions | Perdues/décalées | Intactes | +| Styles | Perdus | Intacts | +| Temps total | Traduction + réfection | Traduction seule | +| Fichier livrable | Non | Oui | + +## Conclusion + +Le problème n'est pas la qualité de la traduction — c'est la **réintégration**. DeepL et Google excellent sur la première ; personne ne fait correctement la seconde, sauf les outils spécialisés. + +Si vos documents comptent (fusions, formules, slides), testez un outil qui travaille *dans* la structure du fichier. Le plan Free d'Office Translator (2 docs/mois, sans carte) est fait pour ça : mettez-lui votre fichier le plus sale, et comparez. + +--- +**Métadonnées SEO** +- Title : « Pourquoi DeepL et Google cassent vos Excel (et comment faire mieux) » +- Meta description : « Formules mortes, fusions perdues : ce que les traducteurs « standard » font à vos fichiers Excel, et l'architecture qui préserve 100 % du format. » +- Mots-clés : deepl excel, google translate excel, excel traduit mise en forme perdue +- URL : /blog/pourquoi-deepl-google-cassent-vos-excel diff --git a/docs/marketing/blog/5-auto-heberger-traduction-docker.md b/docs/marketing/blog/5-auto-heberger-traduction-docker.md new file mode 100644 index 0000000..9c6bcea --- /dev/null +++ b/docs/marketing/blog/5-auto-heberger-traduction-docker.md @@ -0,0 +1,165 @@ +# Auto-héberger son outil de traduction de documents : guide Docker complet + +> Article SEO « self-hosted » — brouillon complet. Capture l'audience développeurs/homelab. Basé sur le déploiement réel du projet (Docker Compose, Postgres, Redis). + +## Introduction + +Un outil de traduction de documents posé sur un SaaS pose toujours la même question : **où vont mes fichiers ?** Pour les documents confidentiels (compta, juridique, RH, brevets), l'auto-hébergement est la seule réponse qui tienne. + +Ce guide montre comment monter une pile complète de traduction de documents — API, base, cache, interface — sur votre propre infrastructure, avec Docker. (Le projet dont on s'inspire est open source : Office Translator / Wordly.art.) + +## Architecture cible + +``` +[Client web / API] + │ + [Nginx (reverse proxy, TLS)] + │ + [FastAPI (Python 3.11+, port 8000)] + ├── [PostgreSQL] ← utilisateurs, glossaires, quotas + ├── [Redis] ← rate limiting (token bucket), files + └── [Fichiers] ← uploads / outputs (volume local) + │ + [Next.js (port 3000)] ← interface +``` + +Points clés : +- **FastAPI** asynchrone pour l'API de traduction (Swagger sur `/docs`) +- **PostgreSQL** pour les données structurées, **Redis** pour le rate limiting par IP et les files +- **Nginx** devant pour le TLS et le reverse proxy +- Volumes persistants pour les fichiers (suppression automatique au TTL, ex. 60 min) + +## 1. Prérequis + +- Une machine Linux (VPS, serveur physique, ou même un NAS) +- Docker + Docker Compose installés +- Un domaine pointant vers la machine (pour le TLS) +- Les clés API de vos moteurs de traduction (au minimum Google ; DeepL, OpenRouter, OpenAI, x.ai/Zai selon vos besoins) + +## 2. Déploiement + +### 2.1 Cloner et configurer + +```bash +git clone /opt/wordly # dépôt à adapter (miroir public recommandé) +cd /opt/wordly +cp .env.example .env +``` + +Dans `.env`, renseignez au minimum : + +```ini +# Moteurs — le Google « classic » (gratuit) ne demande AUCUNE clé. +# Clés optionnelles pour les moteurs payants : +GOOGLE_CLOUD_API_KEY=... +# ou DEEPL_API_KEY, OPENROUTER_API_KEY, OPENAI_API_KEY... +# Optionnel : OCR Mistral pour les PDF scannés +# MISTRAL_API_KEY=... + +# Sécurité +SECRET_KEY= +CORS_ORIGINS=https://wordly.example +APP_ENV=production +``` + +> **Sécurité** : `CORS_ORIGINS` ne doit jamais être `*` en production — l'application le refuse et refuse de démarrer. Générez `SECRET_KEY` avec `openssl rand -hex 32`. + +### 2.2 Lancer la pile + +```bash +docker compose up -d --build +``` + +Ça démarre : +- L'API sur `:8001` côté hôte (mappée sur le 8000 du conteneur ; docs Swagger sur `http://localhost:8001/docs`) +- L'interface sur `:3000` +- Postgres + Redis en interne + +### 2.3 Mettre Nginx devant (TLS) + +```nginx +server { + listen 443 ssl http2; + server_name wordly.example; + + ssl_certificate /etc/letsencrypt/live/wordly.example/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/wordly.example/privkey.pem; + + # Headers de sécurité (HSTS, CSP) + add_header Strict-Transport-Security "max-age=63072000" always; + add_header X-Content-Type-Options nosniff always; + + location / { + proxy_pass http://127.0.0.1:3000; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + location /api/ { + proxy_pass http://127.0.0.1:8001; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + } +} +``` + +Renouvelez le certificat avec `certbot renew` (cron). + +## 3. Exploitation + +### Surveillance + +- `GET /health` — état de l'API, des moteurs, de la base et du cache +- Prometheus + Grafana (fournis en `docker-compose.monitoring.yml`) pour les métriques système et applicatives + +### Sauvegarde (non négociable) + +La base Postgres et le répertoire de fichiers sont votre patrimoine. Deux couches : + +1. **Sauvegarde quotidienne de la base** : + ```bash + docker compose exec db sh -c 'pg_dump -U ${POSTGRES_USER:-translate} ${POSTGRES_DB:-translate_db}' | gzip > /backups/wordly-$(date +%F).sql.gz + ``` +2. **Réplication vers un NAS/autre site** via `rsync` + SSH (le projet fournit un plan de sauvegarde automatique et une procédure de restauration en ~20 min, y compris bascule sur un serveur de secours) + +Règle d'or : **une sauvegarde qu'on n'a jamais restaurée est une sauvegarde qui n'existe pas**. Testez la restauration au moins une fois par trimestre. + +### Nettoyage automatique + +Les fichiers uploadés sont supprimés automatiquement après leur TTL (60 min par défaut). Le nettoyage est orchestré côté application ; surveillez le disque quand même (un gros fichier en attente peut saturer le volume). + +## 4. Durcissement + +- [ ] `CORS_ORIGINS` restreint à vos domaines +- [ ] `SECRET_KEY` unique et stockée en variable d'environnement (jamais dans le repo) +- [ ] Pas de port Postgres/Redis exposé publiquement (réseau Docker interne uniquement) +- [ ] Rate limiting actif (token bucket dans Redis, par IP client) +- [ ] Fail2ban sur SSH si l'accès serveur passe par SSH +- [ ] Mises à jour de Docker + des images (vulnérabilités) +- [ ] Monitoring des disques, CPU et mémoire (Grafana) + +## 5. Coût vs SaaS + +| Poste | Self-hosted (VPS 4 vCPU / 8 Go) | SaaS | +|---|---|---| +| Infrastructure | ~40–80 €/mois | Inclus | +| Coût par page | Coût des API uniquement | Coût API + marge | +| Maîtrise des données | Totale | Partagée | +| Maintenance | À votre charge | À charge de l'éditeur | +| Mises à jour / sécurité | À votre charge | À charge de l'éditeur | + +Le self-hosted gagne quand **les données sont sensibles** ou quand le **volume est énorme**. Il perd quand vous n'avez personne pour faire la maintenance — dans ce cas, le SaaS est souvent plus fiable *en pratique*, même si les fichiers transigent avec un tiers. + +## Conclusion + +Auto-héberger une pile de traduction de documents, c'est 1 h de déploiement et une vraie responsabilité d'exploitation. Avec Docker Compose, Postgres, Redis et Nginx, l'architecture tient sur une seule machine ; ce qui coûte ensuite, c'est la discipline (sauvegardes, durcissement, mises à jour). + +Si les documents sont confidentiels, cette discipline se paie d'elle-même. + +--- +**Métadonnées SEO** +- Title : « Auto-héberger un outil de traduction de documents : guide Docker 2026 » +- Meta description : « FastAPI, Postgres, Redis, Nginx : déployez une pile de traduction de documents sur votre propre infra. Sauvegardes, durcissement, coût vs SaaS. » +- Mots-clés : auto héberger traduction, docker traduction documents, self hosted translation +- URL : /blog/auto-heberger-traduction-documents-docker diff --git a/docs/marketing/kpis.md b/docs/marketing/kpis.md new file mode 100644 index 0000000..3a4b676 --- /dev/null +++ b/docs/marketing/kpis.md @@ -0,0 +1,72 @@ +# Suivi des KPIs — Définitions & Cadence + +> Document de mesure. Source de vérité pour le reporting marketing. +> Mis à jour : 2026-08-29 + +## 1. Stack de mesure (déployée) + +| Outil | Rôle | Où | +|---|---|---| +| **Vercel Analytics** | Pages, événements, referrers, heatmaps | `office-translator-landing-page/app/layout.tsx` | +| **Événements custom** | Funnels précis (voir §2) | `track(...)` dans les composants | +| **Endpoint waitlist** | Source de vérité des e-mails | `GET /api/v1/waitlist/count` | +| **BDD utilisateurs** | Inscriptions, plans, quotas | Backend (Postgres) | +| **Journal de jobs** | Documents traduits, temps de traitement | Backend | +| **Stripe** | MRR, abonnements, churn | Portail Stripe | +| **`/health` + Grafana** | Performance système/applicative | Backend | + +## 2. Événements trackés (Vercel Analytics) + +| Événement | Propriétés | Déclenché par | Funnel | +|---|---|---|---| +| `translation_started` | `engine`, `source`, `target` | Bouton « Translate Document » | Activation | +| `pricing_cta_click` | `plan` | CTA d'un plan | Considération | +| `waitlist_joined` | `interest` | Inscription waitlist réussie | Capture | +| `waitlist_error` | `status` | Inscription waitlist en échec | Capture (santé) | + +**Pages clés à surveiller** : `/` (hero, pricing, faq, waitlist), `/dashboard` (activation), `/admin` (ops). + +## 3. Définitions des KPIs + +| KPI | Définition | Source | Fréquence | +|---|---|---|---| +| Visiteurs uniques | Sessions sur `/` (30 j glissants) | Vercel Analytics | Hebdo | +| Taux d'activation | `translation_started` / visiteurs uniques | Vercel Analytics | Hebdo | +| E-mails waitlist | `count` (dédoublonné) | `/api/v1/waitlist/count` | Quotidien | +| Taux de capture | `waitlist_joined` / visiteurs uniques | Vercel Analytics | Hebdo | +| Inscriptions | Nouveaux comptes (tous plans) | BDD utilisateurs | Hebdo | +| Documents traduits | Jobs terminés | Journal de jobs | Hebdo | +| Taux de conversion payant | Abonnements payants / inscriptions | Stripe + BDD | Mensuel | +| MRR | Revenu mensuel récurrent | Stripe | Mensuel | +| Churn mensuel | Abonnements perdus / début de période | Stripe | Mensuel | +| CAC (Ads) | Dépense Ads / nouveaux payants (30 j) | Google Ads + Stripe | Mensuel | +| NPS | Enquête post-traduction (0–10) | Enquête | Mensuel | +| Disponibilité | `uptime` de `/health` | Grafana | Continu | + +## 4. Cibles (alignées sur MARKETING_PLAN.md §6) + +| KPI | M1 | M3 | +|---|---|---| +| Visiteurs uniques | 5 000 | 25 000 | +| E-mails waitlist | 150 | 1 500 | +| Inscriptions | 200 | 1 500 | +| Documents traduits | 500 | 5 000 | +| Conversion visite→inscription | 2 % | 4 % | +| NPS | > 40 | > 50 | +| MRR | — | ~300 abonnements payants | +| CAC (Ads) | — | < 3× marge mensuelle (plan d'entrée) | + +## 5. Rituels + +- **Hebdo (lun, 30 min)** : tableau de bord Vercel + compteur waitlist + inscriptions. Anomalie > 2 σ = ticket. +- **Mensuel (J+2, 1 h)** : MRR, churn, CAC, NPS. Mise à jour de ce doc + de `MARKETING_PLAN.md` si écart > 20 % vs cible. +- **Sprint (2 sem.)** : A/B sur le pricing et le hero ; décision sur Google Ads (maintien/stop) à la semaine 5. + +## 6. Alertes + +| Condition | Action | +|---|---| +| `waitlist_error` > 5 % des tentatives | Investiguer `POST /api/v1/waitlist` (CORS, prod) | +| Disponibilité `/health` < 99,5 % sur 24 h | Pager ops, consulter `DISASTER_RECOVERY.md` | +| Churn > 10 % / mois | Analyse des raisons de sortie, offre de rétention | +| CAC > 3× marge (Ads) | Stopper la campagne, revenir au SEO/partenariats | diff --git a/docs/marketing/launch/assets-spec.md b/docs/marketing/launch/assets-spec.md new file mode 100644 index 0000000..36d0c00 --- /dev/null +++ b/docs/marketing/launch/assets-spec.md @@ -0,0 +1,61 @@ +# Spec de Production des Assets Visuels + +> Checklist de production des assets marketing. Priorités : P0 = bloquant pour le lancement, P1 = semaine 1, P2 = semaine 2. + +## P0 — Bloquant pour le lancement + +### 1. Captures d'écran (8) — PNG, 1280×720 minimum, fond clair + sombre +| # | Capture | Usage | Source | +|---|---|---|---| +| 1 | Page d'accueil / hero (drop-zone) | Landing, réseaux | `office-translator-landing-page` | +| 2 | Upload en cours (fichier chargé, langues sélectionnées) | Démo du workflow | Idem | +| 3 | **Résultat côte à côte** (original vs traduit, format intact) | Preuve qualité | Backend + dashboard | +| 4 | Sélecteur de moteur (Google/DeepL/LLM) | Fonctionnalité clé | Idem | +| 5 | Interface glossaire | Différenciation | Dashboard | +| 6 | Dashboard (statistiques, quota) | Crédibilité | `/dashboard` | +| 7 | Page pricing (4 plans) | Conversion | Landing `#pricing` | +| 8 | Profil / historique | Confiance | Dashboard | + +**Outil recommandé** : Playwright (script de capture automatisée pour les états dynamiques : upload en cours, progression de traduction). + +### 2. Vidéo démo 60–90 s (« How it works ») — MP4 1080p, sous-titres FR + EN +Script : +1. (0–5 s) Logo + tagline animé +2. (5–15 s) Problème : « Traduire un Excel de 50 pages sans casser le format ? » +3. (15–40 s) Démo accélérée : upload → langues → moteur → traduction → téléchargement +4. (40–55 s) Split-screen original/traduit, zoom sur tableaux et images intacts +5. (55–65 s) CTA « Essayez gratuitement » + URL + +### 3. OG Image — 1200×630 px +Logo + tagline + capture résultat côte à côte. Utilisée pour X, LinkedIn, Product Hunt, partages. + +### 4. GIF workflow 15 s +Boucle upload → traduction → téléchargement (extraire de la vidéo démo). Pour le thread X et Product Hunt. + +## P1 — Semaine 1 + +### 5. Logo +SVG version claire + sombre, icône seule + avec texte « Office Translator » / « Wordly.art ». + +### 6. Favicon +32×32 et 16×16, ICO + PNG. + +### 7. Bannière GitHub +1280×640 px pour le README du repo. + +### 8. Infographie « Pourquoi Office Translator » +Comparatif avant/après (pipeline standard vs en place), 1 colonne, partageable. + +### 9. Tutoriel YouTube 3–5 min +Screencast + voiceover : création de compte, upload, glossaires, moteurs, téléchargement. + +## P2 — Semaine 2 + +### 10. Templates e-mails (5) +Bienvenue, « votre premier document », éducation glossaire, offre d'upgrade, réactivation. + +### 11. Kit réseaux sociaux +12 visuels (4 avant/après, 4 astuces, 4 mises à jour) au format 1080×1350 (LinkedIn) + 1600×900 (X). + +## Validation +Chaque asset P0 est validé par : (a) exactitude technique (pas de capture d'un état impossible), (b) lisibilité au format cible, (c) cohérence de la charte (couleurs `--accent` oklch(0.555 0.17 250), police Geist). diff --git a/docs/marketing/launch/product-hunt.md b/docs/marketing/launch/product-hunt.md new file mode 100644 index 0000000..03fb417 --- /dev/null +++ b/docs/marketing/launch/product-hunt.md @@ -0,0 +1,49 @@ +# Product Hunt — Listing & Maker Comment + +> Statut : prêt à publier (Phase 2, J0). À remplir avec les assets réels avant le lancement. + +## Tagline +**Translate your Excel, Word, PowerPoint & PDF — keep the format perfect.** + +## Nom affiché +Office Translator (by Wordly.art) + +## Description courte (160 car.) +Translate office documents without losing the layout. Merged cells, formulas, tables, slides — translated in place, by the engine you choose. + +## Description longue +Google and DeepL flatten your files into plain text. We don't. + +Office Translator translates Excel, Word, PowerPoint **and PDF** **in place**: merged cells, formulas, fonts, borders, headers, footers, tables and slide layouts survive intact. Even scanned PDFs are handled via OCR (Mistral), and text inside images is translated via vision models. + +Pick the engine per document — 7 providers: +- **Google** (free, fast) +- **DeepL** (best for business text) +- **Google Cloud** +- **LLM engines**: OpenRouter (eco & premium tiers: DeepSeek, Gemini, Claude), OpenAI, Grok by xAI — for complex, context-aware translations + +Plus: +- **Custom glossaries** — lock your HVAC, legal or medical terminology across every document +- **60+ languages** +- **Private by design** — uploads deleted after 30 min, results auto-deleted within 2 h, never used to train models +- **API** on the Business plan (10,000 calls/mo) for your own tooling + +Pricing: Free (2 docs/mo) · Starter €9 · Pro €19 · Business €49 · Enterprise custom. Yearly = −20%. + +## Maker comment (à publier par le fondateur, 9 h EST) +> We built Office Translator because every other tool broke our layouts. +> +> The hard part wasn't translation — it was **re-integrating** the text. Our pipeline parses the document structure (cells, runs, shapes), extracts only translatable content, translates it (your choice of 7 engines), and writes it back into the **same** XML structure. Formulas, merges, fonts, borders: untouched. +> +> What I'd love your feedback on: +> 1. Which engine do you trust most for business documents — DeepL or an LLM? +> 2. Would you use the API (Business plan) to wire it into your own workflow? +> 3. PDF (incl. scanned, via OCR) is already in. Which format should we tackle next — IDML, CSV, something else? +> +> Free plan = 2 full documents/month, no card. Link in comments. + +## Checklist de publication +- [ ] Assets: logo (1200×630 OG + 600×400), 3 screenshots (résultat côte à côte, sélecteur moteur, glossaire) +- [ ] 5 commentaires amis planifiés (réponses de qualité, pas de upvote-only) +- [ ] E-mail aux 150+ contacts de la waitlist (J-2) : « On lance demain, votez + testez » +- [ ] Thread X croisé au lancement diff --git a/docs/marketing/launch/reddit-posts.md b/docs/marketing/launch/reddit-posts.md new file mode 100644 index 0000000..8081e90 --- /dev/null +++ b/docs/marketing/launch/reddit-posts.md @@ -0,0 +1,65 @@ +# Reddit — 3 Posts de Lancement + +> Statut : prêts à publier (Phase 2). Adapter le ton par subreddit. Pas de lien brut dans le 1er post sur r/SideProject (shadowban) — lien en commentaire. + +--- + +## 1. r/SideProject + +**Titre :** I built a tool that translates Excel/Word/PowerPoint without destroying the formatting. Free plan = 2 docs/mo. + +**Corps :** +> Hi all — I've been building **Office Translator** for a while and I'm launching it this week. +> +> **The problem:** every translation tool I tried (Google, DeepL) flattens your file into plain text. Merged cells gone, formulas replaced by their computed values, slide layouts destroyed. You end up spending more time fixing the format than the translation saved you. +> +> **What I did instead:** the pipeline parses the document structure, extracts only the translatable content, translates it, and writes it back into the **same** structure. Merges, formulas, fonts, borders, headers/footers, table styles — untouched. Text inside images is handled by vision models. +> +> **Tech:** FastAPI (async), openpyxl / python-docx / python-pptx, 7 translation engines the user picks per document (Google, DeepL, Google Cloud, OpenRouter éco & premium, OpenAI, Grok/xAI), Stripe billing, Docker/Postgres/Redis. +> +> **Pricing:** Free (2 docs/mo, no card) → Starter €9 → Pro €19 → Business €49 (API included). +> +> I'd love feedback from anyone who deals with international documents. What's the nastiest format you've tried to translate? +> +> (Link in comments to avoid the filter.) + +**Commentaire n°1 (lien) :** +> Here's the landing page: [URL]. Free plan, no card. The /docs page has the full API reference if you want to poke at the API. + +--- + +## 2. r/translator + +**Titre :** We built a document translator that preserves formatting — curious how you'd use it (or wouldn't) + +**Corps :** +> Hi, I'm the dev behind Office Translator (Wordly.art). Before I pitch anything: we know translators are the ones who actually feel the pain when a machine mangles a layout, so I'd rather ask than sell. +> +> The tool translates .xlsx / .docx / .pptx **in place** — structure, formulas, tables and styles are preserved — and supports **custom glossaries**, which is the feature I'd value your opinion on most: +> +> 1. Is glossary-based consistency (e.g. a fixed HVAC or legal term list) actually useful to you, or do you already solve this differently? +> 2. For which document types is the format preservation worth paying for, vs. where a human review still beats any tool? +> 3. What would make you trust an output enough to skip a full re-read? (confidence scores? diff view? per-segment override?) +> +> It's aimed at agencies and in-house teams as a **first pass** to cut turnaround — not to replace the translator. I'd genuinely like to hear what would make it a tool you'd recommend to a colleague. + +> ⚠️ Ce subreddit est hostile au self-promo : publier en tant que dev transparent, répondre à chaque commentaire, jamais de lien dans le post. + +--- + +## 3. r/smallbusiness + +**Titre :** How do you translate client documents (Excel/Word/PowerPoint) without losing the formatting? We built a tool for this. + +**Corps :** +> Running a business that works internationally means a lot of documents that need translating — pricing sheets, contracts, slide decks, training files. +> +> The usual options all hurt somewhere: +> - **Human translators:** great quality, 50–100× the cost and a multi-day turnaround +> - **Google/DeepL:** cheap and fast, but they flatten the file — merged cells, formulas, slide layouts get destroyed, so someone has to rebuild the document +> +> We built **Office Translator** to kill that middle step: it translates the document **in place** and gives you back a file that looks like it was written in the target language from the start. You pick the engine (7 options, from free Google to premium LLMs), and you can lock your company terminology with a custom glossary so "unité de froid" is never suddenly "cold unit". +> +> Free plan: 2 documents/month, no card. From €9/mo after that. Uploads are deleted after 30 minutes and results within 2 hours. +> +> If you handle international documents, what's your current workflow and where does it break? Curious what I'm missing. diff --git a/docs/marketing/launch/show-hn.md b/docs/marketing/launch/show-hn.md new file mode 100644 index 0000000..5ddd1c2 --- /dev/null +++ b/docs/marketing/launch/show-hn.md @@ -0,0 +1,31 @@ +# Hacker News — Show HN + +> Statut : prêt à publier (Phase 2, J0 matin EST). Soumettre via « Show HN » — pas de self-upvote. + +## Titre +**Show HN: Office document translation that preserves the formatting** + +## Corps +> I built **Office Translator** (Wordly.art) because every document translator I used — Google, DeepL, the one-shot tools — destroyed the layout. Translating a 50-page Excel pricing matrix meant getting back a text dump: merged cells gone, formulas replaced by computed values, slide decks unrecognizable. The translation was 5% of the job; fixing the format was 95%. +> +> So I made the format-preservation the product: +> +> - **In-place translation** for .xlsx / .docx / .pptx / .pdf. The pipeline parses the native structure (cells + merges + formulas, paragraph runs + styles, slide XML + shapes), extracts only translatable content, translates it, and writes it back into the *same* structure. Output looks like it was authored in the target language. +> - **Scanned PDFs work too**: image-only pages are recovered with OCR (Mistral) before translation — most competitors (DeepL, Azure) reject those outright. +> - **7 engines, user's choice per document**: Google (free), DeepL, Google Cloud, OpenRouter LLMs in two tiers (DeepSeek / Gemini / Claude), OpenAI, and Grok (xAI). Cheap engine for drafts, premium LLM for client deliverables. +> - **Custom glossaries** to lock technical/legal/medical terminology across documents. +> - **Vision translation** for text inside images (paid plans). +> - **Privacy**: uploads deleted after 30 min, results auto-deleted within 2 hours, zero data retention, content never used for training. +> - **API** on the Business plan (10k calls/mo) — the web workflow is fully mirrored: submit file, poll job, download. +> +> Stack: FastAPI (Python 3.11), openpyxl / python-docx / python-pptx, PyMuPDF, Postgres + Redis, Stripe, Docker, Next.js 15 frontend. +> +> Free plan: 2 documents/month, no card. I'd love feedback from people who've worked on document-format-preserving pipelines — what edge cases do I not handle yet? (PDF incl. scanned/OCR is already in — next candidates are IDML and CSV.) +> +> https://wordly.art + +## Notes de publication +- Soumettre à 8–9 h EST, mardi–jeudi +- Répondre à chaque commentaire dans les 2 h ; rester technique, pas commercial +- Pas de « upvote my post » — c'est interdit sur HN +- Préparer 2–3 réponses techniques de poches : formule Excel + localisation (format de nombre), fusion de cellules + traduction, gestion des runs mixtes (gras/italique au milieu d'un paragraphe) diff --git a/docs/marketing/launch/x-thread.md b/docs/marketing/launch/x-thread.md new file mode 100644 index 0000000..27b4974 --- /dev/null +++ b/docs/marketing/launch/x-thread.md @@ -0,0 +1,102 @@ +# X / Twitter — Thread de Lancement (12 tweets) + +> Statut : prêt à publier (Phase 2, J0). Publier en thread (Reply chain), pas en post unique. +> Assets requis : GIF 15 s du workflow (voir assets-spec.md), capture résultat côte à côte. + +**T1 (accroche + GIF)** +Your Excel got translated. +Now fix the 47 broken merged cells, 12 dead formulas and the slide deck that lost its layout. + +That's what "free" translation tools actually cost. + +We built the opposite. Thread: + +**T2 (le problème)** +Every translator you've used works the same way: +1. Flatten your document to plain text +2. Translate the text +3. Hope it fits back + +The "hope" step is where your afternoon goes. + +**T3 (notre approche)** +Office Translator works in place: + +• Parse the real structure (cells, runs, shapes) +• Extract ONLY the translatable content +• Translate it +• Write it back into the SAME structure + +Merges, formulas, fonts, borders: untouched. + +**T4 (preuve)** +[Capture d'écran : résultat côte à côte — original FR vs traduit EN, mêmes colonnes/fusions/formules] + +Same file. Different language. Zero reformatting. + +**T5 (multi-moteurs)** +You pick the engine per document — 7 options: + +• Google — free, fast +• DeepL — best for business text +• Google Cloud +• DeepSeek / Gemini / Claude via OpenRouter — eco & premium LLM tiers +• OpenAI, Grok (xAI) + +Cheap for drafts. Premium LLM for the client deliverable. + +**T6 (glossaires)** +The feature agencies love: custom glossaries. + +Lock "unité de froid → chiller", "clause de résiliation → termination clause" once. Every document after that uses your terminology. Consistent, every time. + +**T7 (confidentialité)** +Your documents are not our training data. + +• Uploads deleted after 30 min, results within 2 h +• Zero data retention +• Encrypted in transit (TLS) +• No human ever sees your content + +**T8 (API)** +Wiring it into your own stack? Business plan includes API access — 10,000 calls/month. + +Submit file → poll job → download. Same workflow as the web app. Docs at /docs. + +**T9 (prix)** +Pricing (yearly = −20%): + +• Free — 2 docs/mo, no card +• Starter — €9 +• Pro — €19 (AI translation, glossaries) +• Business — €49 (API, 5 seats) +• Enterprise — custom + +A page translated costs a fraction of a cent. A human does the same page for €1–2. + +**T10 (pour qui)** +Built for: +• International SMEs (pricing sheets, contracts, manuals) +• Translation agencies (first-pass + glossaries = faster turnaround) +• Multilingual HR teams +• Anyone who's ever re-built a translated Excel by hand + +**T11 (preuve sociale + CTA)** +60+ languages. 7 engines. 100% format preservation. + +Free plan = 2 full documents/month, no card required. Test it on your ugliest file. + +**T12 (CTA final)** +👉 wordly.art + +Launching on Product Hunt today — feedback welcome. + +What's the nastiest document format you've ever tried to translate? Reply below, we read everything. + +--- + +## Plan de publication +- J-2 : teaser T1 seul (sans lien) +- J0 : thread complet, 9 h +- J+1 : republier T4 (preuve) + répondre à tous les retweets +- Communautés à taguer : #BuildInPublic #SaaS #i18n #DevTools (max 2 hashtags) diff --git a/frontend/src/app/admin/ProviderStatus.tsx b/frontend/src/app/admin/ProviderStatus.tsx index 602bc2c..4756f73 100644 --- a/frontend/src/app/admin/ProviderStatus.tsx +++ b/frontend/src/app/admin/ProviderStatus.tsx @@ -24,6 +24,7 @@ const PROVIDER_LABELS: Record = { deepseek: "IA Express", minimax: "IA Avancée", zai: "Grok (xAI)", + mistral_ocr: "OCR Mistral (PDF scannés)", }; const STATUS_CONFIG = { @@ -132,6 +133,11 @@ export function ProviderStatus({ data, isLoading }: ProviderStatusProps) { {new Date(provider.last_check).toLocaleTimeString()} )} + {provider.config_only && ( + + Statut dérivé de la configuration (pas d'appel réseau) + + )} {provider.error && ( {provider.error} )} diff --git a/frontend/src/app/admin/settings/page.tsx b/frontend/src/app/admin/settings/page.tsx index 2993ad6..3fc8eb8 100644 --- a/frontend/src/app/admin/settings/page.tsx +++ b/frontend/src/app/admin/settings/page.tsx @@ -42,6 +42,7 @@ interface SettingsConfig { openrouter: ProviderConfig; openrouter_premium: ProviderConfig; zai: ProviderConfig; + mistral: ProviderConfig; smtp: SmtpConfig; fallback_chain: string; fallback_chain_classic: string; @@ -54,6 +55,7 @@ interface EnvInfo { openrouter: boolean; openrouter_premium: boolean; zai: boolean; + mistral: boolean; ollama: boolean; google_cloud: boolean; smtp: boolean; @@ -74,6 +76,7 @@ const defaultConfig: SettingsConfig = { openrouter: { enabled: false, api_key: "", model: "deepseek/deepseek-chat" }, openrouter_premium: { enabled: false, api_key: "", model: "openai/gpt-4o-mini" }, zai: { enabled: false, api_key: "", base_url: "https://api.x.ai/v1", model: "grok-2-1212" }, + mistral: { enabled: false, api_key: "", model: "mistral-ocr-latest", timeout: 180 }, smtp: { enabled: false, host: "", port: 587, username: "", password: "", from_email: "", use_tls: true }, fallback_chain: "google,google_cloud,deepl,openrouter,openrouter_premium,openai,deepseek,zai", fallback_chain_classic: "google,google_cloud,deepl", @@ -86,6 +89,7 @@ const defaultEnvInfo: EnvInfo = { openrouter: false, openrouter_premium: false, zai: false, + mistral: false, ollama: false, google_cloud: false, smtp: false, @@ -608,6 +612,42 @@ export default function AdminSettingsPage() { + updateProvider("mistral", { enabled })} + onTest={() => testProvider("mistral")} + testResult={testResults.mistral ?? "idle"} + testMessage={testMessages.mistral} + envKeySet={envInfo.mistral} + > +
+
+ + updateProvider("mistral", { api_key: e.target.value })} + /> +
+
+ + updateProvider("mistral", { model: e.target.value })} + /> +

+ Recommandé : mistral-ocr-latest +

+
+
+
+ Chaîne de fallback diff --git a/frontend/src/app/admin/types.ts b/frontend/src/app/admin/types.ts index 4f71256..263f60f 100644 --- a/frontend/src/app/admin/types.ts +++ b/frontend/src/app/admin/types.ts @@ -30,6 +30,10 @@ export interface ProviderStatus { last_check: string | null; latency_ms?: number; error?: string; + /** true when availability is derived from configuration (no live call) */ + config_only?: boolean; + /** optional display label provided by the backend */ + label?: string; } export interface CleanupResponse { diff --git a/middleware/cleanup.py b/middleware/cleanup.py index f60ac5f..e8e7d4e 100644 --- a/middleware/cleanup.py +++ b/middleware/cleanup.py @@ -38,6 +38,9 @@ class FileCleanupManager: self.max_file_age_seconds = max_file_age_minutes * 60 self.cleanup_interval = cleanup_interval_minutes * 60 self.max_total_size_bytes = int(max_total_size_gb * 1024 * 1024 * 1024) + # Untracked (orphan) files must be at least this old before deletion: + # protects in-flight files whose Redis metadata is missing or delayed. + self.orphan_grace_seconds = 900 self._running = False self._task: Optional[asyncio.Task] = None @@ -191,10 +194,13 @@ class FileCleanupManager: data = await redis_client.get(key) if data: metadata = json.loads(data) - if "file_path" in metadata: - # Normalize path to absolute string for comparison - path_str = str(Path(metadata["file_path"]).absolute()) - tracked_paths.add(path_str) + # Metadata uses "input_path" (and possibly other *_path keys); + # collect every path field so tracked files are never + # misclassified as orphans. + for path_key in ("input_path", "file_path", "output_path"): + if path_key in metadata: + path_str = str(Path(metadata[path_key]).absolute()) + tracked_paths.add(path_str) except Exception as e: logger.warning(f"Failed to fetch tracked paths from Redis: {e}") redis_available = False @@ -234,8 +240,11 @@ class FileCleanupManager: reason = "" if is_orphan: - should_delete = True - reason = "orphan" + # Never delete a young file as orphan: it may belong to + # a job whose Redis metadata is not yet visible. + if file_age > self.orphan_grace_seconds: + should_delete = True + reason = "orphan" elif file_age > self.max_file_age_seconds: should_delete = True reason = "expired" diff --git a/middleware/validation.py b/middleware/validation.py index deec6a5..e4be2c7 100644 --- a/middleware/validation.py +++ b/middleware/validation.py @@ -4,7 +4,7 @@ Validates all user inputs before processing """ import re -import magic +import sys import ipaddress import socket from pathlib import Path @@ -13,6 +13,19 @@ from typing import Optional, List, Set, Tuple from fastapi import UploadFile, HTTPException import logging +# python-magic shells out to libmagic (a native library). It is known to +# crash with an access violation on some Windows setups — a native crash a +# try/except cannot catch. The ZIP/PDF magic-byte checks below do not need +# libmagic, so it is simply disabled on Windows and treated as optional +# elsewhere (a failed import degrades to the same magic-byte fallback). +if sys.platform == "win32": + magic = None +else: + try: + import magic + except Exception: + magic = None + logger = logging.getLogger(__name__) @@ -303,13 +316,17 @@ class FileValidator: def _detect_mime_type(self, content: bytes) -> str: """Detect MIME type from file content""" try: - mime = magic.Magic(mime=True) - return mime.from_buffer(content) + if magic is not None: + mime = magic.Magic(mime=True) + return mime.from_buffer(content) except Exception: - # Fallback to basic detection - if content.startswith(self.OFFICE_MAGIC_BYTES): - return "application/zip" - return "application/octet-stream" + pass + # Fallback to basic detection (magic bytes, no libmagic needed) + if content.startswith(self.OFFICE_MAGIC_BYTES): + return "application/zip" + if content.startswith(self.PDF_MAGIC_BYTES): + return "application/pdf" + return "application/octet-stream" def _validate_mime_type(self, mime_type: str, extension: str): """Validate MIME type matches extension""" @@ -446,41 +463,127 @@ class LanguageValidator: } LANGUAGE_NAMES = { - "en": "English", - "es": "Spanish", - "fr": "French", - "de": "German", - "it": "Italian", - "pt": "Portuguese", - "ru": "Russian", + "af": "Afrikaans", + "sq": "Albanian", + "am": "Amharic", + "ar": "Arabic", + "hy": "Armenian", + "az": "Azerbaijani", + "eu": "Basque", + "be": "Belarusian", + "bn": "Bengali", + "bs": "Bosnian", + "bg": "Bulgarian", + "ca": "Catalan", + "ceb": "Cebuano", "zh": "Chinese", "zh-CN": "Chinese (Simplified)", "zh-TW": "Chinese (Traditional)", - "ja": "Japanese", - "ko": "Korean", - "ar": "Arabic", - "hi": "Hindi", - "nl": "Dutch", - "pl": "Polish", - "tr": "Turkish", - "sv": "Swedish", - "da": "Danish", - "no": "Norwegian", - "fi": "Finnish", + "co": "Corsican", + "hr": "Croatian", "cs": "Czech", + "da": "Danish", + "nl": "Dutch", + "en": "English", + "eo": "Esperanto", + "et": "Estonian", + "fi": "Finnish", + "fr": "French", + "fy": "Frisian", + "gl": "Galician", + "ka": "Georgian", + "de": "German", "el": "Greek", - "th": "Thai", - "vi": "Vietnamese", - "id": "Indonesian", - "uk": "Ukrainian", - "ro": "Romanian", + "gu": "Gujarati", + "ht": "Haitian Creole", + "ha": "Hausa", + "haw": "Hawaiian", + "he": "Hebrew", + "hi": "Hindi", + "hmn": "Hmong", "hu": "Hungarian", + "is": "Icelandic", + "ig": "Igbo", + "id": "Indonesian", + "ga": "Irish", + "it": "Italian", + "ja": "Japanese", + "jv": "Javanese", + "kn": "Kannada", + "kk": "Kazakh", + "km": "Khmer", + "rw": "Kinyarwanda", + "ko": "Korean", + "ku": "Kurdish", + "ky": "Kyrgyz", + "lo": "Lao", + "la": "Latin", + "lv": "Latvian", + "lt": "Lithuanian", + "lb": "Luxembourgish", + "mk": "Macedonian", + "mg": "Malagasy", + "ms": "Malay", + "ml": "Malayalam", + "mt": "Maltese", + "mi": "Maori", + "mr": "Marathi", + "mn": "Mongolian", + "my": "Myanmar (Burmese)", + "ne": "Nepali", + "no": "Norwegian", + "ny": "Nyanja (Chichewa)", + "or": "Odia (Oriya)", + "ps": "Pashto", + "fa": "Persian (Farsi)", + "pl": "Polish", + "pt": "Portuguese", + "pa": "Punjabi", + "ro": "Romanian", + "ru": "Russian", + "sm": "Samoan", + "gd": "Scots Gaelic", + "sr": "Serbian", + "st": "Sesotho", + "sn": "Shona", + "sd": "Sindhi", + "si": "Sinhala", + "sk": "Slovak", + "sl": "Slovenian", + "so": "Somali", + "es": "Spanish", + "su": "Sundanese", + "sw": "Swahili", + "sv": "Swedish", + "tl": "Filipino (Tagalog)", + "tg": "Tajik", + "ta": "Tamil", + "tt": "Tatar", + "te": "Telugu", + "th": "Thai", + "tr": "Turkish", + "tk": "Turkmen", + "uk": "Ukrainian", + "ur": "Urdu", + "ug": "Uyghur", + "uz": "Uzbek", + "vi": "Vietnamese", + "cy": "Welsh", + "xh": "Xhosa", + "yi": "Yiddish", + "yo": "Yoruba", + "zu": "Zulu", "auto": "Auto-detect", } @classmethod def validate(cls, language_code: str, field_name: str = "language") -> str: - """Validate and normalize language code""" + """Validate and normalize language code. + + Matching is case-insensitive so that regional variants stored in + mixed case ("zh-CN", "zh-TW") can be submitted in any case; the + canonical form from SUPPORTED_LANGUAGES is returned. + """ if not language_code: raise ValidationError(f"{field_name} est requis", code="missing_language") @@ -489,9 +592,17 @@ class LanguageValidator: # Handle common variations if normalized in ["chinese", "cn"]: - normalized = "zh-CN" + normalized = "zh-cn" elif normalized in ["chinese-traditional", "tw"]: - normalized = "zh-TW" + normalized = "zh-tw" + + # Resolve to the canonical form stored in SUPPORTED_LANGUAGES + # (e.g. "zh-cn" -> "zh-CN"). Unknown codes stay lower-cased and are + # rejected by the membership check below. + for lang in cls.SUPPORTED_LANGUAGES: + if lang.lower() == normalized: + normalized = lang + break if normalized not in cls.SUPPORTED_LANGUAGES: raise ValidationError( diff --git a/office-translator-landing-page/.gitignore b/office-translator-landing-page/.gitignore index 46214ea..58d1ac1 100644 --- a/office-translator-landing-page/.gitignore +++ b/office-translator-landing-page/.gitignore @@ -7,4 +7,6 @@ __v0_jsx-dev-runtime.ts node_modules/ .next/ .env*.local -.DS_Store \ No newline at end of file +.DS_Store +# TypeScript incremental build artifact +tsconfig.tsbuildinfo diff --git a/office-translator-landing-page/app/page.tsx b/office-translator-landing-page/app/page.tsx index 02ce476..05a45e7 100644 --- a/office-translator-landing-page/app/page.tsx +++ b/office-translator-landing-page/app/page.tsx @@ -1,6 +1,10 @@ import { SiteHeader } from "@/components/site-header" import { HeroSection } from "@/components/hero-section" import { TranslationCard } from "@/components/translation-card" +import { SocialProofSection } from "@/components/social-proof-section" +import { PricingSection } from "@/components/pricing-section" +import { WaitlistSection } from "@/components/waitlist-section" +import { FaqSection } from "@/components/faq-section" export default function Home() { return ( @@ -9,10 +13,39 @@ export default function Home() {
+ + + +
-