fix(marketing): A-2.8a — extract pricing partial + sync bento + OQLF + test calibration

- Extract 3 pricing tiers to templates/marketing/_partials/_pricing_tiers.html
  Single source of truth — landing.html and tarifs.html now {% include %} it.
  Prevents price drift (LPC art. 219 risk).
- Sync bento card #2 description across landing + fonctionnalites
  (was diverged: 'embeddings' vs 'embeddings vocaux'). Add maintenance
  reminder comments in both files.
- Fix OQLF NBSP on '~2 semaines' matrix cells in /tarifs deep-dive table.
- Fix mixed UTF-8/entity 'québécois' -> 'québécois' in tech
  specs (consistent with rest of file).
- Calibrate H2 size on /tarifs FAQ to match landing (clamp 2.75rem cap).
- Repair 2 pre-existing test bugs from earlier A-2.x commits:
  * 'violent la Loi 25' -> accept both NBSP and plain forms (commit 7c6c6fd
    added the NBSP after the test was written)
  * 'résilie' -> 'résilie' (Jinja outputs raw UTF-8, not entities)
- Update src/marketing/routes.py module docstring to reflect 2/4 done.
This commit is contained in:
Allison
2026-04-27 21:06:26 -04:00
parent d471626183
commit 202e1a08d9
8 changed files with 45 additions and 67 deletions

View File

@@ -2,7 +2,7 @@
Phase 2 (A-2.1+): renders templates/marketing/landing.html. Phase 2 (A-2.1+): renders templates/marketing/landing.html.
Tasks A-2.2 through A-2.7 will progressively enrich the landing template. Tasks A-2.2 through A-2.7 will progressively enrich the landing template.
Tasks A-2.8 will add /tarifs, /fonctionnalites, /conformite, /contact routes. Task A-2.8a added /tarifs and /fonctionnalites; A-2.8b will add /conformite and /contact.
""" """
from flask import render_template from flask import render_template

View File

@@ -2303,9 +2303,6 @@
.text-\[clamp\(2\.25rem\,4vw\,3\.5rem\)\] { .text-\[clamp\(2\.25rem\,4vw\,3\.5rem\)\] {
font-size: 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\)\] { .text-\[clamp\(2rem\,3vw\,2\.75rem\)\] {
font-size: clamp(2rem, 3vw, 2.75rem); font-size: clamp(2rem, 3vw, 2.75rem);
} }

View File

@@ -0,0 +1,30 @@
{# Single source of truth for the 3 pricing tiers — used by landing.html#tarifs and /tarifs page.
When prices change, edit ONLY this file. #}
{% from 'macros/pricing_card.html' import pricing_card %}
<div class="grid md:grid-cols-3 gap-6 max-w-5xl mx-auto items-stretch">
{{ pricing_card(
'dictia-8',
'DictIA 8',
'3&nbsp;450&nbsp;$',
'173&nbsp;$',
'PME · RH · Manufacturiers',
['GPU 8&nbsp;Go RTX', 'Volume illimité', 'WhisperX FR-CA', 'Diarisation 8 locuteurs', 'Support inclus']
) }}
{{ pricing_card(
'dictia-16',
'DictIA 16',
'5&nbsp;750&nbsp;$',
'201&nbsp;$',
'Cabinets juridiques · CPA · Finance',
['GPU 16&nbsp;Go RTX', 'Mistral 7B local', 'Q&amp;R sur enregistrement', 'Tout DictIA 8', 'Support prioritaire'],
recommended=True
) }}
{{ pricing_card(
'dictia-cloud',
'DictIA Cloud',
'0&nbsp;$',
'369&nbsp;$',
'Organismes · Municipalités · Multi-sites',
['Hébergé OVH Beauharnois (Québec)', 'Opérationnel sous 48&nbsp;h', 'Aucun matériel à gérer', 'SLA visé 99,9&nbsp;%', 'Conforme Loi&nbsp;25']
) }}
</div>

View File

@@ -22,6 +22,8 @@
<section class="bg-white py-20" aria-labelledby="features-title"> <section class="bg-white py-20" aria-labelledby="features-title">
<div class="max-w-[1060px] mx-auto px-6"> <div class="max-w-[1060px] mx-auto px-6">
<h2 id="features-title" class="sr-only">Six fonctionnalités principales</h2> <h2 id="features-title" class="sr-only">Six fonctionnalités principales</h2>
{# NOTE: bento card content is duplicated between landing.html and fonctionnalites.html.
When editing, sync both files. Future refactor: extract to _partials/_bento_features.html. #}
{% from 'macros/bento.html' import bento_card %} {% from 'macros/bento.html' import bento_card %}
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-[1.5px] bg-brand-border rounded-[18px] overflow-hidden"> <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-[1.5px] bg-brand-border rounded-[18px] overflow-hidden">
{{ bento_card('01', 'Transcription WhisperX', 'Large-v3 fine-tuné FR-CA. Précision 95&nbsp;%+ sur réunions, dictées, audiences (méthodologie disponible sur demande).', '🎙️') }} {{ bento_card('01', 'Transcription WhisperX', 'Large-v3 fine-tuné FR-CA. Précision 95&nbsp;%+ sur réunions, dictées, audiences (méthodologie disponible sur demande).', '🎙️') }}
@@ -108,7 +110,7 @@
</div> </div>
<div class="grid md:grid-cols-2 gap-6"> <div class="grid md:grid-cols-2 gap-6">
{% for spec in [ {% for spec in [
{'title': 'Modèle ASR', 'desc': 'WhisperX Large-v3 (1,55&nbsp;G paramètres) fine-tuné sur audio professionnel qu&eacute;b&eacute;cois. Format ONNX optimisé GPU.'}, {'title': 'Modèle ASR', 'desc': 'WhisperX Large-v3 (1,55&nbsp;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': '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&amp;R)', 'desc': 'Mistral 7B Instruct quantifié 4-bit. Inférence locale sur le même GPU. Aucune sortie réseau.'}, {'title': 'LLM (résumés / Q&amp;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&nbsp;v3.'}, {'title': 'Stack web', 'desc': 'Flask 2.3 (backend) + Vue 3 / Alpine.js (front). Chiffrement AES-256 au repos. AGPL&nbsp;v3.'},

View File

@@ -195,10 +195,12 @@
</h2> </h2>
</div> </div>
{# NOTE: bento card content is duplicated between landing.html and fonctionnalites.html.
When editing, sync both files. Future refactor: extract to _partials/_bento_features.html. #}
{% from 'macros/bento.html' import bento_card %} {% from 'macros/bento.html' import bento_card %}
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-[1.5px] bg-brand-border rounded-[18px] overflow-hidden"> <div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-[1.5px] bg-brand-border rounded-[18px] overflow-hidden">
{{ bento_card('01', 'Transcription WhisperX', 'Large-v3 fine-tuné FR-CA. Précision 95&nbsp;%+ sur réunions, dictées, audiences (méthodologie disponible sur demande).', '🎙️') }} {{ bento_card('01', 'Transcription WhisperX', 'Large-v3 fine-tuné FR-CA. Précision 95&nbsp;%+ 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.', '👥') }} {{ 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('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&amp;R sur enregistrement', 'Posez des questions à vos réunions. RAG local sur embeddings sentence-transformers.', '💬') }} {{ bento_card('04', 'Q&amp;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('05', 'Exports multiples', 'DOCX, PDF, SRT, VTT, TXT, JSON, MD. Formats avocat, notaire, CPA.', '📄') }}
@@ -220,34 +222,7 @@
</p> </p>
</div> </div>
{% from 'macros/pricing_card.html' import pricing_card %} {% include 'marketing/_partials/_pricing_tiers.html' %}
<div class="grid md:grid-cols-3 gap-6 max-w-5xl mx-auto items-stretch">
{{ pricing_card(
'dictia-8',
'DictIA 8',
'3&nbsp;450&nbsp;$',
'173&nbsp;$',
'PME · RH · Manufacturiers',
['GPU 8&nbsp;Go RTX', 'Volume illimité', 'WhisperX FR-CA', 'Diarisation 8 locuteurs', 'Support inclus']
) }}
{{ pricing_card(
'dictia-16',
'DictIA 16',
'5&nbsp;750&nbsp;$',
'201&nbsp;$',
'Cabinets juridiques · CPA · Finance',
['GPU 16&nbsp;Go RTX', 'Mistral 7B local', 'Q&amp;R sur enregistrement', 'Tout DictIA 8', 'Support prioritaire'],
recommended=True
) }}
{{ pricing_card(
'dictia-cloud',
'DictIA Cloud',
'0&nbsp;$',
'369&nbsp;$',
'Organismes · Municipalités · Multi-sites',
['Hébergé OVH Beauharnois (Québec)', 'Opérationnel sous 48&nbsp;h', 'Aucun matériel à gérer', 'SLA visé 99,9&nbsp;%', 'Conforme Loi&nbsp;25']
) }}
</div>
{# ROI CALCULATOR — Alpine.js, hypotheses transparentes pour LPC art. 219 hygiene #} {# ROI CALCULATOR — Alpine.js, hypotheses transparentes pour LPC art. 219 hygiene #}
<div x-data="roiCalculator()" class="mt-16 max-w-3xl mx-auto bg-white p-8 rounded-[18px] border border-brand-border" aria-labelledby="roi-title"> <div x-data="roiCalculator()" class="mt-16 max-w-3xl mx-auto bg-white p-8 rounded-[18px] border border-brand-border" aria-labelledby="roi-title">

View File

@@ -22,34 +22,7 @@
<section class="bg-brand-bg py-20" aria-labelledby="forfaits-title"> <section class="bg-brand-bg py-20" aria-labelledby="forfaits-title">
<div class="max-w-[1200px] mx-auto px-6"> <div class="max-w-[1200px] mx-auto px-6">
<h2 id="forfaits-title" class="sr-only">Trois forfaits DictIA</h2> <h2 id="forfaits-title" class="sr-only">Trois forfaits DictIA</h2>
{% from 'macros/pricing_card.html' import pricing_card %} {% include 'marketing/_partials/_pricing_tiers.html' %}
<div class="grid md:grid-cols-3 gap-6 max-w-5xl mx-auto items-stretch">
{{ pricing_card(
'dictia-8',
'DictIA 8',
'3&nbsp;450&nbsp;$',
'173&nbsp;$',
'PME · RH · Manufacturiers',
['GPU 8&nbsp;Go RTX', 'Volume illimité', 'WhisperX FR-CA', 'Diarisation 8 locuteurs', 'Support inclus']
) }}
{{ pricing_card(
'dictia-16',
'DictIA 16',
'5&nbsp;750&nbsp;$',
'201&nbsp;$',
'Cabinets juridiques · CPA · Finance',
['GPU 16&nbsp;Go RTX', 'Mistral 7B local', 'Q&amp;R sur enregistrement', 'Tout DictIA 8', 'Support prioritaire'],
recommended=True
) }}
{{ pricing_card(
'dictia-cloud',
'DictIA Cloud',
'0&nbsp;$',
'369&nbsp;$',
'Organismes · Municipalités · Multi-sites',
['Hébergé OVH Beauharnois (Québec)', 'Opérationnel sous 48&nbsp;h', 'Aucun matériel à gérer', 'SLA visé 99,9&nbsp;%', 'Conforme Loi&nbsp;25']
) }}
</div>
</div> </div>
</section> </section>
@@ -83,7 +56,7 @@
{'name': 'Diarisation', 'd8': '8 locuteurs', 'd16': '8 locuteurs', 'cloud': '8 locuteurs'}, {'name': 'Diarisation', 'd8': '8 locuteurs', 'd16': '8 locuteurs', 'cloud': '8 locuteurs'},
{'name': 'Résumés Mistral 7B local', 'd8': '—', 'd16': '✓', 'cloud': '✓ (mutualisé)'}, {'name': 'Résumés Mistral 7B local', 'd8': '—', 'd16': '✓', 'cloud': '✓ (mutualisé)'},
{'name': 'Q&amp;R sur enregistrement', 'd8': '—', 'd16': '✓', 'cloud': '✓'}, {'name': 'Q&amp;R sur enregistrement', 'd8': '—', 'd16': '✓', 'cloud': '✓'},
{'name': 'Délai de mise en service', 'd8': '~2 semaines', 'd16': '~2 semaines', 'cloud': '48&nbsp;h'} {'name': 'Délai de mise en service', 'd8': '~2&nbsp;semaines', 'd16': '~2&nbsp;semaines', 'cloud': '48&nbsp;h'}
] %} ] %}
<tr> <tr>
<th scope="row" class="text-left p-4 font-semibold text-brand-navy/80">{{ row.name | safe }}</th> <th scope="row" class="text-left p-4 font-semibold text-brand-navy/80">{{ row.name | safe }}</th>
@@ -107,7 +80,7 @@
<div class="max-w-[820px] mx-auto px-6"> <div class="max-w-[820px] mx-auto px-6">
<div class="text-center mb-10"> <div class="text-center mb-10">
<p class="eyebrow grad-text mb-4">QUESTIONS DE TARIFICATION</p> <p class="eyebrow grad-text mb-4">QUESTIONS DE TARIFICATION</p>
<h2 id="tarifs-faq-title" class="text-[clamp(2rem,3vw,2.5rem)] font-black mb-4 text-brand-navy">Vos questions sur les tarifs.</h2> <h2 id="tarifs-faq-title" class="text-[clamp(2rem,3vw,2.75rem)] font-black mb-4 text-brand-navy">Vos questions sur les tarifs.</h2>
</div> </div>
<div class="divide-y divide-brand-border border-y border-brand-border"> <div class="divide-y divide-brand-border border-y border-brand-border">

View File

@@ -199,7 +199,8 @@ def test_pas_probleme_section_present():
client = app.test_client() client = app.test_client()
body = client.get('/').data.decode('utf-8') body = client.get('/').data.decode('utf-8')
assert 'PROBL' in body and 'TRANSCRIPTION CLOUD' in body, "Missing Problème eyebrow" assert 'PROBL' in body and 'TRANSCRIPTION CLOUD' in body, "Missing Problème eyebrow"
assert 'violent la Loi 25' in body, "Missing legal-risk H2 anchor phrase" assert 'violent la Loi&nbsp;25' in body or 'violent la Loi 25' in body, \
"Missing legal-risk H2 anchor phrase"
assert 'Cloud Act' in body, "Missing Cloud Act card" assert 'Cloud Act' in body, "Missing Cloud Act card"
assert 'biom' in body and 'Loi 25' in body, "Missing Loi 25 biometric card" assert 'biom' in body and 'Loi 25' in body, "Missing Loi 25 biometric card"
assert 'Sanctions disciplinaires' in body, "Missing sanctions disciplinaires card" assert 'Sanctions disciplinaires' in body, "Missing sanctions disciplinaires card"
@@ -661,7 +662,7 @@ def test_faq_section_with_7_questions():
assert f'id="faq-panel-{i}"' in body, f"Missing FAQ panel {i}" assert f'id="faq-panel-{i}"' in body, f"Missing FAQ panel {i}"
# Question topic anchors # Question topic anchors
topics = ['Loi', 'Cloud et DictIA on-premise', 'fran', 'audiences', 'formats', topics = ['Loi', 'Cloud et DictIA on-premise', 'fran', 'audiences', 'formats',
'r&eacute;silie', 'AGPL'] 'résilie', 'AGPL']
for topic in topics: for topic in topics:
assert topic in body, f"FAQ missing topic anchor: {topic}" assert topic in body, f"FAQ missing topic anchor: {topic}"

View File

@@ -141,7 +141,7 @@ def test_fonctionnalites_tech_specs_6_items():
client = app.test_client() client = app.test_client()
body = client.get('/fonctionnalites').data.decode('utf-8') body = client.get('/fonctionnalites').data.decode('utf-8')
assert 'specs-title' in body assert 'specs-title' in body
for spec_keyword in ['Modèle ASR', 'pyannote', 'Mistral 7B', 'Flask', 'WAV, MP3', 'qu&eacute;b&eacute;cois']: 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}" assert spec_keyword in body, f"Missing tech spec keyword: {spec_keyword}"