From d47162618319c20898c01f136f31830a3edd96f1 Mon Sep 17 00:00:00 2001 From: Allison Date: Mon, 27 Apr 2026 20:50:07 -0400 Subject: [PATCH] feat(marketing): A-2.8a /tarifs + /fonctionnalites standalone pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /tarifs page: extends base.html, reuses pricing_card + button macros, shows the 3 forfaits with NBSP-formatted CAD prices, an 8-row deep-dive comparison matrix (DictIA 8 vs DictIA 16 vs DictIA Cloud), 5 tarification FAQ items (frais cachés, migration, GPU, taxes TPS/TVQ, plans annuels) with Alpine accordion + focus-visible WCAG 2.2 AA, CTA section - /fonctionnalites page: extends base.html, reuses bento_card macro, re-renders the 6 features with same content as landing's bento section for consistency, adds dedicated 7-format export grid + 8-integration grid (with trademark disclaimer) + 6 tech specs section (Whisper/pyannote /Mistral/stack/audio/langues), CTA section - routes.py: add /tarifs and /fonctionnalites routes (passes FAQ to /tarifs for the tarification accordion; preserves existing landing(), TESTIMONIALS, FAQ data structures unchanged) - tests/test_marketing_secondary_pages.py: NEW test file (16 tests covering routes 200, base.html inheritance, H1 anchors, 3 pricing cards, comparison matrix, tarifs FAQ accordion, OQLF typography, 6 bento + 7 exports + 8 integrations + 6 tech specs sections, canonical meta) - All sections respect WCAG 2.2 AA, FlexiHub design discipline, LPC art. 219 hygiene (sourcing dates, trademark disclaimer, hedged claims, NBSP) --- src/marketing/routes.py | 16 ++ static/css/marketing.css | 21 +++ templates/marketing/fonctionnalites.html | 144 ++++++++++++++++++ templates/marketing/tarifs.html | 163 +++++++++++++++++++++ tests/test_marketing_secondary_pages.py | 178 +++++++++++++++++++++++ 5 files changed, 522 insertions(+) create mode 100644 templates/marketing/fonctionnalites.html create mode 100644 templates/marketing/tarifs.html create mode 100644 tests/test_marketing_secondary_pages.py diff --git a/src/marketing/routes.py b/src/marketing/routes.py index 0e687cb..f2b5f57 100644 --- a/src/marketing/routes.py +++ b/src/marketing/routes.py @@ -77,3 +77,19 @@ def landing(): testimonials=TESTIMONIALS, faq=FAQ, ) + + +@marketing_bp.route('/tarifs') +def tarifs(): + """Standalone pricing page — same 3 forfaits as landing /#tarifs anchor, + plus deep-dive comparison matrix and tarification FAQ. + """ + return render_template('marketing/tarifs.html', faq=FAQ) + + +@marketing_bp.route('/fonctionnalites') +def fonctionnalites(): + """Standalone features page — deep-dive on the 6 bento features + plus full integrations list and supported export formats. + """ + return render_template('marketing/fonctionnalites.html') diff --git a/static/css/marketing.css b/static/css/marketing.css index 7e6ba40..add2a8b 100644 --- a/static/css/marketing.css +++ b/static/css/marketing.css @@ -1334,6 +1334,12 @@ .rounded-\[0\.75rem\] { border-radius: 0.75rem; } + .rounded-\[12px\] { + border-radius: 12px; + } + .rounded-\[14px\] { + border-radius: 14px; + } .rounded-\[18px\] { border-radius: 18px; } @@ -2297,6 +2303,9 @@ .text-\[clamp\(2\.25rem\,4vw\,3\.5rem\)\] { font-size: clamp(2.25rem, 4vw, 3.5rem); } + .text-\[clamp\(2rem\,3vw\,2\.5rem\)\] { + font-size: clamp(2rem, 3vw, 2.5rem); + } .text-\[clamp\(2rem\,3vw\,2\.75rem\)\] { font-size: clamp(2rem, 3vw, 2.75rem); } @@ -3220,6 +3229,13 @@ } } } + .hover\:bg-brand-navy\/\[0\.03\] { + &:hover { + @media (hover: hover) { + background-color: color-mix(in oklab, #060d1a 3%, transparent); + } + } + } .hover\:bg-emerald-700 { &:hover { @media (hover: hover) { @@ -4285,6 +4301,11 @@ grid-template-columns: repeat(4, minmax(0, 1fr)); } } + .lg\:grid-cols-7 { + @media (width >= 64rem) { + grid-template-columns: repeat(7, minmax(0, 1fr)); + } + } .lg\:grid-cols-9 { @media (width >= 64rem) { grid-template-columns: repeat(9, minmax(0, 1fr)); diff --git a/templates/marketing/fonctionnalites.html b/templates/marketing/fonctionnalites.html new file mode 100644 index 0000000..2421a10 --- /dev/null +++ b/templates/marketing/fonctionnalites.html @@ -0,0 +1,144 @@ +{% extends 'marketing/base.html' %} + +{% block title %}Fonctionnalités DictIA — Transcription IA WhisperX, diarisation, résumés Mistral 7B local{% endblock %} +{% block description %}Toutes les fonctionnalités de DictIA : WhisperX Large-v3 fine-tuné FR-CA, diarisation 8 locuteurs, Mistral 7B local, Q&R RAG, exports DOCX/PDF/SRT, intégrations Word/Outlook/Teams.{% endblock %} + +{% block content %} + +{# ===== HEADER ===== #} +
+
+

FONCTIONNALITÉS

+

+ Tout ce qu'il faut pour transcrire en restant chez soi. +

+

+ Une pile technique 100 % québécoise, sans dépendance à un fournisseur cloud américain. Détails techniques sur demande : info@dictia.ca. +

+
+
+ +{# ===== 6 BENTO FEATURES ===== #} +
+
+

Six fonctionnalités principales

+ {% from 'macros/bento.html' import bento_card %} +
+ {{ bento_card('01', 'Transcription WhisperX', 'Large-v3 fine-tuné FR-CA. Précision 95 %+ sur réunions, dictées, audiences (méthodologie disponible sur demande).', '🎙️') }} + {{ bento_card('02', 'Diarisation 8 locuteurs', 'pyannote sépare automatiquement les intervenants. Identification par embeddings vocaux.', '👥') }} + {{ bento_card('03', 'Résumés Mistral 7B', 'IA locale génère résumés, points d\'action et procès-verbaux. Aucune connexion cloud.', '📝') }} + {{ bento_card('04', 'Q&R sur enregistrement', 'Posez des questions à vos réunions. RAG local sur embeddings sentence-transformers.', '💬') }} + {{ bento_card('05', 'Exports multiples', 'DOCX, PDF, SRT, VTT, TXT, JSON, MD. Formats avocat, notaire, CPA.', '📄') }} + {{ bento_card('06', 'Intégrations', 'Word, Outlook, Teams, Notion, Obsidian, Zapier, Make, n8n.', '🔌') }} +
+
+
+ +{# ===== EXPORT FORMATS DEEP-DIVE ===== #} +
+
+
+

FORMATS D'EXPORT

+

+ 7 formats, prêts pour vos workflows. +

+
+
+ {% for fmt in [ + {'ext': 'DOCX', 'use': 'Word avec timestamps cliquables'}, + {'ext': 'PDF', 'use': 'Procès-verbaux signés'}, + {'ext': 'SRT', 'use': 'Sous-titres vidéo'}, + {'ext': 'VTT', 'use': 'Web/streaming standard'}, + {'ext': 'TXT', 'use': 'Texte brut universel'}, + {'ext': 'JSON', 'use': 'Pipeline développeur'}, + {'ext': 'MD', 'use': 'Notion, Obsidian, GitHub'} + ] %} +
+

{{ fmt.ext }}

+

{{ fmt.use | safe }}

+
+ {% endfor %} +
+
+
+ +{# ===== INTEGRATIONS GRID ===== #} +
+
+
+

INTÉGRATIONS

+

+ Branchez DictIA à votre stack existant. +

+

+ Webhooks REST, plugin Word natif, connecteurs Zapier/Make/n8n. API documentée — auto-hébergée avec votre déploiement. +

+
+
+ {% for integ in [ + {'name': 'Microsoft Word', 'desc': 'Plugin natif (.docx)'}, + {'name': 'Microsoft Outlook', 'desc': 'Pièces jointes audio'}, + {'name': 'Microsoft Teams', 'desc': 'Enregistrements de réunions'}, + {'name': 'Notion', 'desc': 'Pages markdown auto'}, + {'name': 'Obsidian', 'desc': 'Notes timestamped'}, + {'name': 'Zapier', 'desc': 'Workflows no-code'}, + {'name': 'Make (Integromat)', 'desc': 'Scénarios visuels'}, + {'name': 'n8n', 'desc': 'Open source self-host'} + ] %} +
+

{{ integ.name | safe }}

+

{{ integ.desc | safe }}

+
+ {% endfor %} +
+

+ Microsoft, Notion, Obsidian, Zapier, Make et n8n sont des marques de leurs propriétaires respectifs. DictIA n'est pas affilié à ces produits. +

+
+
+ +{# ===== TECH SPECS ===== #} +
+
+
+

SPÉCIFICATIONS TECHNIQUES

+

+ Sous le capot. +

+
+
+ {% for spec in [ + {'title': 'Modèle ASR', 'desc': 'WhisperX Large-v3 (1,55 G paramètres) fine-tuné sur audio professionnel québécois. Format ONNX optimisé GPU.'}, + {'title': 'Diarisation', 'desc': 'pyannote 3.x — clustering hiérarchique sur embeddings ECAPA-TDNN. 1 à 8 locuteurs détectés automatiquement.'}, + {'title': 'LLM (résumés / Q&R)', 'desc': 'Mistral 7B Instruct quantifié 4-bit. Inférence locale sur le même GPU. Aucune sortie réseau.'}, + {'title': 'Stack web', 'desc': 'Flask 2.3 (backend) + Vue 3 / Alpine.js (front). Chiffrement AES-256 au repos. AGPL v3.'}, + {'title': 'Audio supportés', 'desc': 'WAV, MP3, M4A, FLAC, OGG, WebM — jusqu\'à 8 h par fichier. Conversion ffmpeg automatique.'}, + {'title': 'Langues', 'desc': 'Optimisé français québécois. Aussi : français de France, anglais (canadien et US), espagnol, allemand, mandarin, russe.'} + ] %} +
+

{{ spec.title | safe }}

+

{{ spec.desc | safe }}

+
+ {% endfor %} +
+
+
+ +{# ===== CTA ===== #} +
+
+

+ Prêt à essayer DictIA ? +

+

+ Lancement printemps 2026. Inscrivez-vous pour figurer sur la liste prioritaire. +

+
+ {% from 'macros/button.html' import button %} + {{ button('Pré-inscription par courriel', href='mailto:info@dictia.ca?subject=Pré-inscription%20DictIA', variant='primary', size='lg', icon='✉️') }} + {{ button('Voir les tarifs', href='/tarifs', variant='secondary', size='lg') }} +
+
+
+ +{% endblock %} diff --git a/templates/marketing/tarifs.html b/templates/marketing/tarifs.html new file mode 100644 index 0000000..e40b2bb --- /dev/null +++ b/templates/marketing/tarifs.html @@ -0,0 +1,163 @@ +{% extends 'marketing/base.html' %} + +{% block title %}Tarifs DictIA — 3 forfaits transparents en CAD (369 $/mois Cloud, à partir de 173 $/mois on-premise){% endblock %} +{% block description %}Tarifs DictIA en CAD : DictIA 8 (PME), DictIA 16 (cabinets juridiques) et DictIA Cloud (organismes). Volume illimité, zéro frais par utilisateur, taxes en sus.{% endblock %} + +{% block content %} + +{# ===== HEADER ===== #} +
+
+

TARIFS

+

+ Trois forfaits : choisissez votre infrastructure. +

+

+ Volume illimité, zéro frais par utilisateur. Tarifs en CAD, taxes en sus (TPS 5 % + TVQ 9,975 %). +

+
+
+ +{# ===== 3 PRICING TIERS ===== #} +
+
+

Trois forfaits DictIA

+ {% from 'macros/pricing_card.html' import pricing_card %} +
+ {{ pricing_card( + 'dictia-8', + 'DictIA 8', + '3 450 $', + '173 $', + 'PME · RH · Manufacturiers', + ['GPU 8 Go RTX', 'Volume illimité', 'WhisperX FR-CA', 'Diarisation 8 locuteurs', 'Support inclus'] + ) }} + {{ pricing_card( + 'dictia-16', + 'DictIA 16', + '5 750 $', + '201 $', + 'Cabinets juridiques · CPA · Finance', + ['GPU 16 Go RTX', 'Mistral 7B local', 'Q&R sur enregistrement', 'Tout DictIA 8', 'Support prioritaire'], + recommended=True + ) }} + {{ pricing_card( + 'dictia-cloud', + 'DictIA Cloud', + '0 $', + '369 $', + 'Organismes · Municipalités · Multi-sites', + ['Hébergé OVH Beauharnois (Québec)', 'Opérationnel sous 48 h', 'Aucun matériel à gérer', 'SLA visé 99,9 %', 'Conforme Loi 25'] + ) }} +
+
+
+ +{# ===== COMPARISON MATRIX ===== #} +
+
+
+

COMPARAISON DÉTAILLÉE

+

+ Détails par forfait. +

+
+ +
+ + + + + + + + + + + + {% for row in [ + {'name': 'Hébergement', 'd8': 'Sur place (vos murs)', 'd16': 'Sur place (vos murs)', 'cloud': 'OVH Beauharnois (QC)'}, + {'name': 'GPU', 'd8': '8 Go RTX', 'd16': '16 Go RTX', 'cloud': 'Mutualisé (géré)'}, + {'name': 'Volume audio', 'd8': 'Illimité', 'd16': 'Illimité', 'cloud': 'Illimité'}, + {'name': 'Utilisateurs', 'd8': 'Illimité', 'd16': 'Illimité', 'cloud': 'Illimité'}, + {'name': 'Diarisation', 'd8': '8 locuteurs', 'd16': '8 locuteurs', 'cloud': '8 locuteurs'}, + {'name': 'Résumés Mistral 7B local', 'd8': '—', 'd16': '✓', 'cloud': '✓ (mutualisé)'}, + {'name': 'Q&R sur enregistrement', 'd8': '—', 'd16': '✓', 'cloud': '✓'}, + {'name': 'Délai de mise en service', 'd8': '~2 semaines', 'd16': '~2 semaines', 'cloud': '48 h'} + ] %} + + + + + + + {% endfor %} + +
Comparaison détaillée des 3 forfaits DictIA sur 8 caractéristiques
CaractéristiqueDictIA 8DictIA 16DictIA Cloud
{{ row.name | safe }}{{ row.d8 | safe }}{{ row.d16 | safe }}{{ row.cloud | safe }}
+
+ +

+ Caractéristiques au 2026-04-27. Pour un devis personnalisé ou des besoins multi-sites, écrivez à info@dictia.ca. +

+
+
+ +{# ===== TARIFICATION FAQ ===== #} +
+
+
+

QUESTIONS DE TARIFICATION

+

Vos questions sur les tarifs.

+
+ +
+ {% for item in [ + {'q': 'Y a-t-il des frais cachés?', 'a': 'Non. Les tarifs affichés couvrent l\'utilisation illimitée (volume audio, utilisateurs, exports). Les seules variables sont : les taxes (TPS 5 % + TVQ 9,975 %) et, pour DictIA on-premise, le matériel GPU si vous ne l\'avez pas déjà. Aucun frais par minute, par mot, par locuteur.'}, + {'q': 'Puis-je passer d\'un forfait à un autre?', 'a': 'Oui, en tout temps. Les passages DictIA Cloud → on-premise et inversement sont supportés. Les données peuvent être migrées sur demande, sans frais. Détails dans nos conditions d\'utilisation.'}, + {'q': 'Le tarif on-premise inclut-il le matériel GPU?', 'a': 'Le tarif setup (3 450 $ pour DictIA 8 ou 5 750 $ pour DictIA 16) inclut l\'installation logicielle complète, la configuration sécurité, la formation et 90 jours de support prioritaire. Le matériel GPU n\'est pas inclus ; nous fournissons une liste de cartes RTX recommandées (RTX 4060 8 Go pour DictIA 8, RTX 4080/5080 16 Go pour DictIA 16) et pouvons faire l\'achat pour vous moyennant marge transparente.'}, + {'q': 'Comment fonctionne la facturation TPS/TVQ?', 'a': 'DictIA Inc. est inscrite TPS et TVQ. Les factures détaillent les taxes selon votre province de facturation. Pour les organismes exemptés (organismes publics, etc.), envoyez votre attestation à info@dictia.ca avant l\'inscription.'}, + {'q': 'Existe-t-il un tarif annuel ou pluriannuel?', 'a': 'Disponible sur demande pour les engagements 12 ou 24 mois (remise typique de 10 à 15 %). Écrivez à info@dictia.ca pour un devis.'} + ] %} +
+

+ +

+
+

{{ item.a | safe }}

+
+
+ {% endfor %} +
+
+
+ +{# ===== CTA ===== #} +
+ +
+

+ Une question sur votre forfait idéal ? +

+

+ Nous accompagnons chaque organisation dans le choix du forfait le mieux adapté à sa volumétrie, ses contraintes réglementaires et son infrastructure existante. Aucune pression commerciale. +

+
+ {% from 'macros/button.html' import button %} + {{ button('Discuter avec notre équipe', href='mailto:info@dictia.ca?subject=Question%20tarifs%20DictIA', variant='primary', size='lg', icon='✉️') }} + {{ button('Voir les fonctionnalités', href='/fonctionnalites', variant='ghost', size='lg') }} +
+
+
+ +{% endblock %} diff --git a/tests/test_marketing_secondary_pages.py b/tests/test_marketing_secondary_pages.py new file mode 100644 index 0000000..5f43824 --- /dev/null +++ b/tests/test_marketing_secondary_pages.py @@ -0,0 +1,178 @@ +"""Tests for the secondary marketing pages (A-2.8).""" +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +os.environ.setdefault('SQLALCHEMY_DATABASE_URI', 'sqlite:///:memory:') +os.environ.setdefault('SECRET_KEY', 'test-secret-key') + +from src.app import app # noqa: E402 + + +# === /tarifs === + +def test_tarifs_route_returns_200(): + client = app.test_client() + response = client.get('/tarifs') + assert response.status_code == 200, "GET /tarifs must return 200" + + +def test_tarifs_extends_marketing_base(): + client = app.test_client() + body = client.get('/tarifs').data.decode('utf-8') + assert '' in body + assert 'lang="fr-CA"' in body + assert '/static/css/marketing.css' in body + assert '/static/js/alpine.min.js' in body + + +def test_tarifs_has_h1_with_anchor(): + client = app.test_client() + body = client.get('/tarifs').data.decode('utf-8') + assert 'page-title' in body + assert '' in body + assert 'scope="col"' in body + assert 'scope="row"' in body + # 8 row keywords + for kw in ['Hébergement', 'GPU', 'Volume audio', 'Utilisateurs', + 'Diarisation', 'Mistral 7B local', 'Q&R', 'Délai']: + assert kw in body, f"Missing matrix row keyword: {kw}" + + +def test_tarifs_pricing_faq_5_questions(): + client = app.test_client() + body = client.get('/tarifs').data.decode('utf-8') + assert 'tarifs-faq-title' in body + for i in range(1, 6): + assert f'tarifs-faq-panel-{i}' in body, f"Missing tarifs FAQ panel {i}" + # Alpine accordion bindings + assert body.count('x-data="{ open: false }"') >= 5 + # Each accordion button has focus-visible (WCAG 2.4.7/2.4.11) + assert 'focus-visible:outline-2' in body + + +def test_tarifs_uses_oqlf_typography(): + client = app.test_client() + body = client.get('/tarifs').data.decode('utf-8') + assert 'TPS 5 %' in body + assert 'TVQ 9,975 %' in body + assert 'Loi 25' in body + # No double-escape + assert '&nbsp;' not in body, "Pricing macro / safe filter regression" + + +# === /fonctionnalites === + +def test_fonctionnalites_route_returns_200(): + client = app.test_client() + response = client.get('/fonctionnalites') + assert response.status_code == 200 + + +def test_fonctionnalites_extends_marketing_base(): + client = app.test_client() + body = client.get('/fonctionnalites').data.decode('utf-8') + assert '' in body + assert 'lang="fr-CA"' in body + assert '/static/css/marketing.css' in body + + +def test_fonctionnalites_h1_present(): + client = app.test_client() + body = client.get('/fonctionnalites').data.decode('utf-8') + assert 'page-title' in body + assert 'rester' in body or 'restant chez soi' in body + + +def test_fonctionnalites_renders_6_bento_cards(): + client = app.test_client() + body = client.get('/fonctionnalites').data.decode('utf-8') + assert 'features-title' in body + # 6 watermark numbers + for n in ['01', '02', '03', '04', '05', '06']: + assert f'>{n}<' in body + # 6 feature anchors + for kw in ['WhisperX', 'Diarisation', 'Mistral 7B', 'RAG local', 'DOCX, PDF, SRT', 'Outlook, Teams']: + assert kw in body + + +def test_fonctionnalites_export_formats_section(): + client = app.test_client() + body = client.get('/fonctionnalites').data.decode('utf-8') + assert 'exports-title' in body + for ext in ['DOCX', 'PDF', 'SRT', 'VTT', 'TXT', 'JSON', 'MD']: + # Each format has its own card with the .ext as a heading + assert f'>{ext}<' in body, f"Missing export format card: {ext}" + + +def test_fonctionnalites_integrations_grid(): + client = app.test_client() + body = client.get('/fonctionnalites').data.decode('utf-8') + assert 'integrations-title' in body + for name in ['Microsoft Word', 'Microsoft Outlook', 'Microsoft Teams', + 'Notion', 'Obsidian', 'Zapier', 'Make', 'n8n']: + assert name in body, f"Missing integration: {name}" + # Trademark disclaimer + assert 'marques de leurs propriétaires' in body or 'marques de leurs propriétaires' in body + + +def test_fonctionnalites_tech_specs_6_items(): + client = app.test_client() + body = client.get('/fonctionnalites').data.decode('utf-8') + assert 'specs-title' in body + for spec_keyword in ['Modèle ASR', 'pyannote', 'Mistral 7B', 'Flask', 'WAV, MP3', 'québécois']: + assert spec_keyword in body, f"Missing tech spec keyword: {spec_keyword}" + + +def test_fonctionnalites_uses_oqlf_typography(): + client = app.test_client() + body = client.get('/fonctionnalites').data.decode('utf-8') + # NBSP entities + assert '95 %+' in body, "WhisperX precision NBSP entity" + assert 'GPU 8 Go RTX' not in body # Bento card calls don't use 8 Go RTX (that's pricing) + assert 'Q&R' in body, "French Q&R (not Q&A)" + # No double-escape + assert '&nbsp;' not in body + + +# === Cross-page checks === + +def test_secondary_pages_in_main_nav(): + """Header nav links to /tarifs and /fonctionnalites — verify both pages now respond.""" + client = app.test_client() + for url in ['/tarifs', '/fonctionnalites']: + # Each page should self-link in its own nav (consistency) + body = client.get(url).data.decode('utf-8') + assert 'href="/tarifs"' in body and 'href="/fonctionnalites"' in body, \ + f"Page {url} must include nav links to both new pages" + + +def test_secondary_pages_have_canonical_meta(): + """Both pages must have canonical URL + OG metadata via base.html.""" + client = app.test_client() + for url in ['/tarifs', '/fonctionnalites']: + body = client.get(url).data.decode('utf-8') + assert 'rel="canonical"' in body + assert 'og:type' in body + assert 'twitter:card' in body