- Transcrivez vos réunions
- sans risquer votre permis.
-
+
+ {# Single-column hero — texte centré (lg : aligné gauche). Le visuel canonique est le 3-step flow inline (pas de mockup app). #}
+
- {# Sub-headline — ≤25 words, value prop #}
-
- DictIA convertit vos audio en texte, résumé et points d'action — 100% au Québec, conforme Barreau, CPA Québec, ChAD et 6 autres ordres professionnels.
-
+ {# Eyebrow / breadcrumb back-link "Toutes les solutions" — link to anchor #solutions sur landing #}
+
+
+ Toutes les solutions
+
- {# Dual CTA — primary (demo) + secondary ghost (pricing) #}
-
- {% from 'macros/button.html' import button %}
- {{ button('Réserver une démo', href='/contact', variant='primary', size='lg') }}
- {{ button('Voir les tarifs', href='/tarifs', variant='ghost', size='lg', icon='') }}
-
+ {# Sous-eyebrow — pillars #}
+
+
+ TRANSCRIPTION IA · CONFORME LOI 25 · QUÉBEC
+
+
+
+
+ Audio → Texte · Résumés IA · Conforme Loi 25 & ordres professionnels
+
- {# Social proof microcopy — defensible: refers to pre-launch waitlist + factual ordres pros count #}
-
+ Transcription IA locale en 2 minutes — conforme Barreau, CPA Québec et ChAD.
+
+
+ {# Sub canonique #}
+
+ DictIA transforme vos réunions en texte, résumés et points d'action en 2 minutes — hébergé au Québec sur OVH Beauharnois, zéro Cloud Act. Contrairement à Teams Copilot ou Otter.ai, vos données ne quittent jamais le Québec. Conforme au Cadre IA MCN et aux 5 ordres à directives IA formelles (Barreau, ChAD, CMQ, OIIQ, OACIQ).
+
+
+ {# Stats grid — 4 colonnes #}
+
+ {% for stat in [
+ ('~2 min', 'Pour 1 h d\'audio'),
+ ('5 ordres','Directives IA formelles'),
+ ('95 %+', 'Précision FR-CA'),
+ ('0 $', 'Frais par utilisateur')
+ ] %}
+
+
{{ stat[0] | safe }}
+
{{ stat[1] | safe }}
+ {% endfor %}
+
- {# The mockup itself — role=img with descriptive label for screen readers #}
-
+ {# Dual CTA — primary "Réserver une démo" magnétique + ghost "Voir les forfaits" #}
+
- Du fichier au résumé en 4 étapes.
+ Du fichier au résumé — en temps réel.
Aucune installation côté utilisateur, aucune conversion préalable. DictIA orchestre l'ensemble du pipeline — du téléversement à l'export — en moins de deux minutes pour une heure d'audio.
+
+ Survolez une fonctionnalité pour voir la machine en action. Glissez pour calculer votre gain de productivité.
+
{# Pipeline track + 4 nodes — Alpine state drives all visuals.
@@ -1235,6 +1225,82 @@
+{# ===== LANGUES + IA LOCALE ===== #}
+{# Section compacte : grille 99+ langues détectées (gauche) + carte IA Mistral 7B LOCAL (droite). #}
+
+
+
+ {# Colonne gauche — 99+ langues #}
+
+
+
+
+
+
+
99+ langues détectées
+
WhisperX Large-v3 · multilingue par défaut
+
+
+
+ {# Grille codes langue — 12 puces (FR mis en avant via grad-bg) #}
+
+ {% for code in ['FR','EN','ES','DE','PT','IT','NL','PL','ZH','JA','KO','AR','RU','HI','TR','VI','TH','SV','DA','NO','FI'] %}
+
+
+ {{ code }}
+
+
+ {% endfor %}
+
+
+
+
+ Auto · Détection automatique de la langue à l'upload
+
+
+
+ {# Colonne droite — IA intégrée Mistral 7B LOCAL #}
+
+ {# Subtle orb décoratif #}
+
+
+
+
+
+
+
+
+
IA intégrée — Mistral 7B (LOCAL)
+
Inférence sur votre GPU · zéro cloud étranger
+
+
+
+
+
+
+ Résumé · Points d'action · Q&R
+
+
+
+ Données hébergées sur vos serveurs · jamais partagées
+
+
+
+ Zéro connexion OpenAI · Google · Microsoft
+
+
+
+ Inférence hors-ligne · résultats en secondes
+
+
+
+
+
+
+
+
{# ===== BENTO FEATURES ===== #}
@@ -1472,6 +1538,10 @@
{% include 'marketing/_partials/_pricing_tiers.html' %}
+
+ Tous les prix en CAD, taxes en sus (TPS 5 % + TVQ 9,975 %).
+
+
{# ROI CALCULATOR — Alpine.js, hypotheses transparentes pour LPC art. 219 hygiene #}
CALCULATEUR ROI
@@ -1633,8 +1703,22 @@
Architecture conçue avec les exigences professionnelles québécoises.
- DictIA mappe son architecture aux cadres réglementaires applicables au secteur public et aux ordres professionnels du Québec. Détails techniques (EFVP, audit trail, déclaration CAI) disponibles sur demande : info@dictia.ca.
+ DictIA mappe son architecture aux cadres réglementaires applicables au secteur public et aux ordres professionnels du Québec. DictIA a été conçu pour les secteurs réglementés du Québec — Loi 25, Cloud Act, Barreau, ChAD, AMF. Détails techniques (EFVP, audit trail, déclaration CAI) disponibles sur demande : info@dictia.ca.
+ Conformité au 19 juin 2026 — vous dirigez un cégep, un CISSS ou un ministère ?
+
+
+ Vous avez jusqu'au 19 juin 2026.
+
+
+ Depuis le 19 décembre 2025, tous les organismes publics québécois doivent appliquer un cadre IA strict. Aucun outil cloud non approuvé ne peut recevoir de renseignements confidentiels — fini ChatGPT ou Teams Copilot dans les CA, les séances cliniques ou les comités universitaires.
+
+
+
+ {# Carte spotlight — Cadre IA MCN détaillé #}
+
+
+
+
+
+
+
+ Cadre IA MCN — Énoncé de principes mis à jour + Indication d'application IAG
+
+
Publié le 19 décembre 2025 sous l'art. 21 LGGRI
+
+
+
+
+ Interdit aux organismes publics (art. 2 LGGRI) d'entrer des renseignements confidentiels dans un système IA non approuvé — ministères, organismes budgétaires, Santé Québec, CISSS/CIUSSS, centres de services scolaires, cégeps, universités. Régime allégé pour entreprises du gouvernement (art. 4 — Hydro-Québec, SAQ, Loto-Québec, CDPQ).
+
+
+
+ {% for bullet in [
+ ('Énoncé de principes — 12 principes éthiques applicables à tout système IA dans l\'administration publique', 0),
+ ('Indication d\'application IAG — gouvernance, gestion des risques, mesures de contrôle, protection des données, formation du personnel', 80),
+ ('Délai conformité : 19 juin 2026 (6 mois post publication 19 déc. 2025)', 160),
+ ('Municipalités, MRC et Assemblée nationale non visées par l\'Énoncé — mais Loi sur l\'accès (A-2.1) reste applicable aux séances publiques', 240),
+ ('Loi 25 — voix = donnée biométrique (LCCJTI art. 44-45), déclaration CAI obligatoire si banque biométrique', 320),
+ ('Loi 96 (C-11) — documents générés en français pour organisations 25+ employés', 400),
+ ('Hébergement au Québec — aucune société US dans la chaîne (Cloud Act inapplicable)', 480)
+ ] %}
+
- Réservez votre pré-inscription.
+ Prêt à protéger vos données ?
-
+
+ Réservez une démonstration gratuite. Nous analyserons vos besoins et vous recommanderons le forfait adapté à votre réalité.
+
+
Lancement printemps 2026. Les premiers utilisateurs bénéficient d'une remise de bienvenue et d'un accompagnement direct par notre équipe technique. Aucun engagement.
{% 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('Réserver ma démo gratuite', href='/contact', variant='primary', size='lg', icon='') }}
+ {{ button('Pré-inscription par courriel', href='mailto:info@dictia.ca?subject=Pré-inscription%20DictIA', variant='ghost', size='lg', icon='') }}
{{ button('Voir les forfaits', href='#tarifs', variant='ghost', size='lg') }}
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..713e2a9
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,25 @@
+"""Test bootstrap — Windows shim for fcntl (used by src/init_db.py on POSIX).
+
+Allows running tests on Windows even though the production app targets Linux.
+Mirrors the stub used by serve_marketing.py for local preview.
+"""
+import os
+import sys
+import types
+
+# Stub fcntl BEFORE pytest collects any test that imports src.app
+if sys.platform.startswith('win') and 'fcntl' not in sys.modules:
+ fcntl_stub = types.ModuleType('fcntl')
+ fcntl_stub.LOCK_EX = 2
+ fcntl_stub.LOCK_NB = 4
+ fcntl_stub.LOCK_UN = 8
+ fcntl_stub.LOCK_SH = 1
+ fcntl_stub.flock = lambda *_a, **_kw: None
+ fcntl_stub.fcntl = lambda *_a, **_kw: 0
+ sys.modules['fcntl'] = fcntl_stub
+
+# Minimal env so src/config/app_config.py doesn't sys.exit on missing config
+os.environ.setdefault('SQLALCHEMY_DATABASE_URI', 'sqlite:///:memory:')
+os.environ.setdefault('SECRET_KEY', 'test-secret-key')
+os.environ.setdefault('TRANSCRIPTION_BASE_URL', 'http://local-stub')
+os.environ.setdefault('TRANSCRIPTION_API_KEY', 'local-stub')
diff --git a/tests/test_marketing_landing_template.py b/tests/test_marketing_landing_template.py
index 11f8af0..8aab7c2 100644
--- a/tests/test_marketing_landing_template.py
+++ b/tests/test_marketing_landing_template.py
@@ -83,22 +83,30 @@ def test_landing_no_login_redirect_for_anonymous():
def test_hero_has_h1_with_grad_text_accent():
- """Hero H1 contains grad-text span on the brand tagline."""
+ """Hero H1 (round 3) contains the brand wordmark with grad-text accent on 'IA'.
+
+ Round 3 replaces the old tagline ('sans risquer votre permis') with the canonical
+ DictIA wordmark + the H2 phrase 'Transcription IA locale en 2 minutes'.
+ """
client = app.test_client()
body = client.get('/').data.decode('utf-8')
assert 'id="hero-title"' in body, "Missing hero-title id on H1"
assert 'grad-text' in body, "Missing grad-text class somewhere"
- assert 'sans risquer votre permis' in body, "Missing key brand tagline"
+ # New canonical brand H2 phrase (cyan/grad on key claim)
+ assert 'Transcription IA locale en 2' in body, "Missing canonical H2 phrase"
+ # Hero word-staggered reveal hook on the wordmark
+ assert 'hero-h1-word' in body, "Missing word-staggered reveal class"
def test_hero_has_dual_cta():
- """Hero has both primary (Réserver une démo) and ghost (Voir les tarifs) CTAs."""
+ """Hero (round 3) has primary (Réserver une démo) and ghost (Voir les forfaits) CTAs."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
assert 'href="/contact"' in body
assert 'href="/tarifs"' in body
assert 'Réserver une démo' in body or 'Réserver une démo' in body
- assert 'Voir les tarifs' in body
+ # Round 3 canonical wording: 'Voir les forfaits' (matches dictia.ca/solutions/dictai)
+ assert 'Voir les forfaits' in body, "Round 3 secondary CTA must say 'Voir les forfaits'"
def test_hero_has_cosmic_orbs_background():
@@ -121,25 +129,28 @@ def test_hero_has_social_proof_microcopy():
def test_hero_has_staggered_animations():
- """Hero elements use tc-fade-in-up with staggered delays."""
+ """Hero (round 3) elements use tc-fade-in-up with staggered delays — canonical cadence.
+
+ Round 3 staggers : 0 (back-link), 75 (eyebrow), 200 (3-step flow),
+ 280 (H2 phrase), 360 (sub), 440 (stats), 520 (CTAs), 600 (social proof).
+ """
client = app.test_client()
body = client.get('/').data.decode('utf-8')
assert 'animate-tc-fade-in-up' in body, "Missing fade-in animation"
- assert 'animation-delay: 0ms' in body
- assert 'animation-delay: 75ms' in body
- assert 'animation-delay: 150ms' in body
- assert 'animation-delay: 300ms' in body
- assert 'animation-delay: 400ms' in body
+ for delay in ['0ms', '75ms', '200ms', '280ms', '360ms', '440ms', '520ms', '600ms']:
+ assert f'animation-delay: {delay}' in body, f"Missing staggered delay {delay}"
assert 'animation-fill-mode: backwards' in body, \
"Missing animation-fill-mode (causes flash before delay fires)"
def test_hero_eyebrow_has_brand_messaging():
- """Hero eyebrow declares the 3 brand pillars."""
+ """Hero eyebrow declares the 3 brand pillars (round 3 uses OQLF NBSP : LOI 25)."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
assert 'TRANSCRIPTION IA' in body
- assert 'CONFORME LOI 25' in body
+ # OQLF-conformant : non-breaking space before "25" (NBSP entity)
+ assert 'CONFORME LOI 25' in body or 'CONFORME LOI 25' in body, \
+ "Missing 'CONFORME LOI 25' eyebrow (with or without NBSP)"
assert 'QU' in body # Either QUÉBEC or QUÉBEC
@@ -665,31 +676,32 @@ def test_testimonials_use_personas_not_fake_names():
assert name not in body, f"Forbidden fabricated testimonial name: {name}"
-def test_faq_section_with_7_questions():
- """FAQ section present with 7 questions."""
+def test_faq_section_with_10_questions():
+ """FAQ section (round 3) present with 10 canonical questions from dictai-page-content.tsx."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
assert 'faq-title' in body
- # 7 panel IDs (loop.index is 1-indexed)
- for i in range(1, 8):
+ # 10 panel IDs (loop.index is 1-indexed)
+ for i in range(1, 11):
assert f'id="faq-panel-{i}"' in body, f"Missing FAQ panel {i}"
- # Question topic anchors
- topics = ['Loi', 'Cloud et DictIA on-premise', 'fran', 'audiences', 'formats',
- 'résilie', 'AGPL']
+ # Round 3 canonical topic anchors (sourced from dictai-page-content.tsx)
+ topics = ['Comment fonctionne la transcription', 'formats audio', '1 heure d',
+ 'confidentielle', 'Teams Copilot', 'Otter.ai', 'Barreau du Qu',
+ 'Clio Manage', 'connaissances techniques', 'open source']
for topic in topics:
assert topic in body, f"FAQ missing topic anchor: {topic}"
def test_faq_alpine_accordion_bindings():
- """FAQ uses Alpine.js x-data + @click + :aria-expanded for accessible accordion."""
+ """FAQ uses Alpine.js x-data + @click + :aria-expanded for accessible accordion (10 items)."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
- # 7 x-data="{ open: false }" instances
- assert body.count('x-data="{ open: false }"') == 7, \
- "FAQ must have 7 independent Alpine accordion items"
+ # 10 x-data="{ open: false }" instances (round 3 enrichment)
+ assert body.count('x-data="{ open: false }"') == 10, \
+ "FAQ must have 10 independent Alpine accordion items"
# Each toggle button has @click and :aria-expanded
- assert body.count('@click="open = !open"') == 7
- assert body.count(':aria-expanded="open.toString()"') == 7
+ assert body.count('@click="open = !open"') == 10
+ assert body.count(':aria-expanded="open.toString()"') == 10
# Use built-in x-transition (NOT x-collapse plugin which is not bundled)
assert 'x-collapse' not in body, "Must NOT use x-collapse plugin (not loaded — use x-transition)"
assert 'x-transition.opacity' in body, "FAQ panels must use built-in x-transition"
@@ -720,7 +732,8 @@ def test_faq_jsonld_schema_present():
assert parsed['@context'] == 'https://schema.org'
assert parsed['@type'] == 'FAQPage'
assert isinstance(parsed['mainEntity'], list)
- assert len(parsed['mainEntity']) == 7, "FAQPage must contain exactly 7 questions"
+ # Round 3: enriched to 10 canonical questions from dictai-page-content.tsx
+ assert len(parsed['mainEntity']) == 10, "FAQPage must contain exactly 10 questions (round 3)"
for q in parsed['mainEntity']:
assert q['@type'] == 'Question'
assert q['acceptedAnswer']['@type'] == 'Answer'
@@ -729,18 +742,25 @@ def test_faq_jsonld_schema_present():
def test_cta_final_section():
- """CTA final section with mailto pré-inscription + ghost button to #tarifs."""
+ """CTA final (round 3) — primary démo gratuite + mailto pré-inscription + ghost button to #tarifs."""
client = app.test_client()
body = client.get('/').data.decode('utf-8')
assert 'cta-title' in body
- assert 'pré-inscription' in body or 'pré-inscription' in body
+ # Round 3 wording reinforced: "Prêt à protéger vos données" + démo gratuite
+ assert 'prot' in body and 'donn' in body, "Missing 'protéger vos données' headline"
+ assert 'démo gratuite' in body or 'démo gratuite' in body, \
+ "Round 3 primary CTA must say 'démo gratuite'"
+ # Pré-inscription wording (any case) preserved as secondary path
+ assert 'pré-inscription' in body or 'pré-inscription' in body \
+ or 'Pré-inscription' in body or 'Pré-inscription' in body, \
+ "Pré-inscription wording must be preserved"
# mailto with subject
assert 'href="mailto:info@dictia.ca?subject=Pr%C3%A9-inscription%20DictIA"' in body or \
'href="mailto:info@dictia.ca?subject=Pré-inscription%20DictIA"' in body, \
"CTA must have mailto with subject prefilled"
# Anchor link to existing #tarifs section
assert 'href="#tarifs"' in body, "Secondary CTA must anchor to pricing"
- # Ghost variant button
+ # Ghost variant button still in use (mailto + #tarifs)
assert 'border-white/[0.08]' in body # ghost button class
@@ -839,12 +859,16 @@ def test_round2_no_external_js_libs_added():
def test_round2_preserves_existing_sections():
- """Round 2 inserts must NOT remove Hero / Pipeline / Hub / Bento / Comparatif / Conformité."""
+ """Round 2 + 3 inserts must NOT remove Hero / Pipeline / Hub / Bento / Comparatif / Conformité.
+
+ NOTE: round 3 replaced the hero copy ('sans risquer votre permis' → canonical wordmark
+ + 'Transcription IA locale en 2 minutes'). The hero ID + pipeline are still required.
+ """
client = app.test_client()
body = client.get('/').data.decode('utf-8')
- # Hero (round 0)
+ # Hero (round 3 canonical hero replaces round 0)
assert 'hero-title' in body
- assert 'sans risquer votre permis' in body
+ assert 'Transcription IA locale en 2' in body, "Round 3 hero canonical phrase missing"
# Pipeline (round 1) — auto-advance + 4 nodes
assert 'pipeline-title' in body
assert 'Du fichier au résumé' in body
@@ -880,7 +904,8 @@ def test_routes_passes_testimonials_and_faq_to_template():
assert hasattr(routes, 'TESTIMONIALS'), "Module must define TESTIMONIALS list"
assert hasattr(routes, 'FAQ'), "Module must define FAQ list"
assert len(routes.TESTIMONIALS) == 3, "Must have 3 placeholder testimonials"
- assert len(routes.FAQ) == 7, "Must have 7 FAQ entries"
+ # Round 3: enriched FAQ from 7 to 10 canonical questions (sourced from dictai-page-content.tsx)
+ assert len(routes.FAQ) == 10, "Must have 10 FAQ entries (round 3)"
# Each testimonial must NOT contain a 'quote' field (no fabricated quotes pre-launch)
for t in routes.TESTIMONIALS:
assert 'quote' not in t, "Pre-launch testimonials must not contain quote fields"