- Restore card_database.md as index (table markdown) instead of inline content - Create 6 missing card files (PLAN-001, PLAN-002, PATTERN-001, PERSONAL-001, FAMILY-001, TECH-001) - Fix ID conflicts: Rename FOOD cards (fiches_nutritionnelles.md → FOOD-001, cuisiner_pour_elle.md → FOOD-002) - Update get_daily_cards.py to parse table + load from individual files (remove inline parsing code) - Total: 29 active cards (was 23 with duplicates) - Update CLAUDE.md with new card count and breakdown Architecture: - card_database.md = INDEX (table with metadata) - cards/*.md = SOURCE OF TRUTH (individual card files) - Scripts read table, then load content from files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
303 lines
9.7 KiB
Python
303 lines
9.7 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Script pour sélectionner les cartes quotidiennes du système Anki Tingting.
|
||
|
||
Usage:
|
||
python get_daily_cards.py [--num-cards N] [--json]
|
||
|
||
Options:
|
||
--num-cards N : Nombre de cartes à sélectionner (défaut: 3)
|
||
--json : Output en JSON (défaut: texte lisible)
|
||
"""
|
||
|
||
import sys
|
||
import os
|
||
import re
|
||
import json
|
||
import argparse
|
||
from datetime import datetime, timedelta
|
||
from pathlib import Path
|
||
|
||
|
||
# Chemins relatifs au script
|
||
SCRIPT_DIR = Path(__file__).parent
|
||
BASE_DIR = SCRIPT_DIR.parent
|
||
CARD_DB_PATH = BASE_DIR / "card_database.md"
|
||
CARDS_DIR = BASE_DIR / "cards"
|
||
|
||
|
||
def parse_date(date_str):
|
||
"""Parse une date au format DD/MM/YYYY ou 'Never'."""
|
||
if date_str == "Never" or date_str == "N/A":
|
||
return None
|
||
try:
|
||
return datetime.strptime(date_str, "%d/%m/%Y")
|
||
except ValueError:
|
||
return None
|
||
|
||
|
||
def parse_card_database():
|
||
"""Parse le fichier card_database.md et retourne la liste des cartes."""
|
||
if not CARD_DB_PATH.exists():
|
||
print(f"❌ ERREUR: Fichier {CARD_DB_PATH} introuvable", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
with open(CARD_DB_PATH, 'r', encoding='utf-8') as f:
|
||
content = f.read()
|
||
|
||
# Trouver la table markdown
|
||
table_pattern = r'\| ID \| Card \| Difficulty.*?\n\|---.*?\n((?:\|.*?\n)+)'
|
||
match = re.search(table_pattern, content, re.DOTALL)
|
||
|
||
if not match:
|
||
print("❌ ERREUR: Impossible de trouver la table dans card_database.md", file=sys.stderr)
|
||
print("ℹ️ Le fichier doit contenir une table markdown avec colonnes:", file=sys.stderr)
|
||
print(" | ID | Card | Difficulty | Frequency | Last Review | Success Rate | Failures | Critical |", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
table_rows = match.group(1).strip().split('\n')
|
||
print(f"ℹ️ INFO: {len(table_rows)} lignes de table trouvées", file=sys.stderr)
|
||
|
||
cards = []
|
||
for i, row in enumerate(table_rows, 1):
|
||
# Parse chaque ligne de la table
|
||
cols = [col.strip() for col in row.split('|')[1:-1]] # Ignore les | de début/fin
|
||
|
||
if len(cols) < 8:
|
||
print(f"⚠️ WARNING: Ligne {i} ignorée (seulement {len(cols)} colonnes au lieu de 8)", file=sys.stderr)
|
||
continue
|
||
|
||
card_id, card_link, difficulty, frequency, last_review, success_rate, times_failed, critical = cols
|
||
|
||
# Extraire le nom du fichier depuis le lien markdown [filename](cards/filename.md)
|
||
filename_match = re.search(r'\[(.*?\.md)\]', card_link)
|
||
if not filename_match:
|
||
print(f"⚠️ WARNING: Impossible d'extraire le filename de: {card_link}", file=sys.stderr)
|
||
continue
|
||
|
||
card_file = filename_match.group(1)
|
||
|
||
cards.append({
|
||
'id': card_id,
|
||
'file': card_file,
|
||
'difficulty': difficulty,
|
||
'frequency': frequency,
|
||
'last_review': parse_date(last_review),
|
||
'last_review_str': last_review,
|
||
'success_rate': success_rate,
|
||
'times_failed': int(times_failed) if times_failed.isdigit() else 0,
|
||
'critical': critical == '⚠️'
|
||
})
|
||
|
||
print(f"✅ SUCCESS: {len(cards)} cartes chargées depuis la table", file=sys.stderr)
|
||
return cards
|
||
|
||
|
||
|
||
|
||
def load_card_content(card_file):
|
||
"""Charge le contenu d'une carte depuis son fichier."""
|
||
card_path = CARDS_DIR / card_file
|
||
|
||
if not card_path.exists():
|
||
print(f"⚠️ WARNING: Fichier {card_file} introuvable dans {CARDS_DIR}", file=sys.stderr)
|
||
return {
|
||
'question': f"[FICHIER MANQUANT: {card_file}]",
|
||
'answer': "Le fichier de cette carte est introuvable",
|
||
'notes': ""
|
||
}
|
||
|
||
try:
|
||
with open(card_path, 'r', encoding='utf-8') as f:
|
||
content = f.read()
|
||
|
||
# Extract question
|
||
question_match = re.search(r'## Question\s*\n\s*\n(.*?)\n\s*\n---', content, re.DOTALL)
|
||
question = question_match.group(1).strip() if question_match else "Question non trouvée"
|
||
|
||
# Extract answer
|
||
answer_match = re.search(r'## Answer\s*\n\s*\n(.*?)\n\s*\n---', content, re.DOTALL)
|
||
answer = answer_match.group(1).strip() if answer_match else "Réponse non trouvée"
|
||
|
||
# Extract notes
|
||
notes_match = re.search(r'## Notes\s*\n\s*\n(.*?)\n\s*\n---', content, re.DOTALL)
|
||
notes = notes_match.group(1).strip() if notes_match else ""
|
||
|
||
return {
|
||
'question': question,
|
||
'answer': answer,
|
||
'notes': notes
|
||
}
|
||
|
||
except Exception as e:
|
||
print(f"⚠️ WARNING: Erreur lors du chargement de {card_file}: {e}", file=sys.stderr)
|
||
return {
|
||
'question': f"[ERREUR: {card_file}]",
|
||
'answer': f"Erreur lors du chargement: {e}",
|
||
'notes': ""
|
||
}
|
||
|
||
|
||
def calculate_priority_score(card, today):
|
||
"""Calcule un score de priorité pour une carte."""
|
||
score = 0
|
||
|
||
# CRITICAL cards = haute priorité
|
||
if card['critical']:
|
||
score += 100
|
||
|
||
# Times failed = haute priorité
|
||
score += card['times_failed'] * 50
|
||
|
||
# Difficulté
|
||
difficulty_scores = {'Hard': 30, 'Medium': 20, 'Easy': 10}
|
||
score += difficulty_scores.get(card['difficulty'], 0)
|
||
|
||
# Temps depuis dernière review
|
||
if card['last_review'] is None:
|
||
score += 200 # Jamais révisée = très haute priorité
|
||
else:
|
||
days_since = (today - card['last_review']).days
|
||
|
||
# Calculer le nombre de jours attendus selon frequency
|
||
freq_days = {
|
||
'Daily': 1,
|
||
'Every 2-3 days': 2,
|
||
'Every 3-4 days': 3,
|
||
'Every 2 weeks': 14,
|
||
'Monthly': 30,
|
||
'Every conflict': 999 # Pas basé sur le temps
|
||
}
|
||
|
||
expected_days = freq_days.get(card['frequency'], 7)
|
||
|
||
if card['frequency'] == 'Every conflict':
|
||
# Cartes de conflit : priorité basse sauf si jamais révisées
|
||
score += 5
|
||
elif days_since >= expected_days:
|
||
# En retard sur la review
|
||
score += 80 + (days_since - expected_days) * 10
|
||
else:
|
||
# Pas encore le moment
|
||
score += 5
|
||
|
||
# Success rate (si disponible)
|
||
if card['success_rate'] not in ['N/A', 'Never']:
|
||
try:
|
||
rate = int(card['success_rate'].replace('%', ''))
|
||
if rate < 70:
|
||
score += 40
|
||
except ValueError:
|
||
pass
|
||
|
||
return score
|
||
|
||
|
||
def select_daily_cards(num_cards=3):
|
||
"""Sélectionne les cartes pour la session quotidienne."""
|
||
cards = parse_card_database()
|
||
today = datetime.now()
|
||
|
||
# Calculer le score de priorité pour chaque carte
|
||
for card in cards:
|
||
card['priority_score'] = calculate_priority_score(card, today)
|
||
|
||
# Trier par priorité décroissante
|
||
cards.sort(key=lambda c: c['priority_score'], reverse=True)
|
||
|
||
# Sélectionner les N premières cartes
|
||
selected = cards[:num_cards]
|
||
|
||
# Charger le contenu de chaque carte depuis les fichiers individuels
|
||
for card in selected:
|
||
content = load_card_content(card['file'])
|
||
card.update(content)
|
||
|
||
return selected
|
||
|
||
|
||
def format_output_text(cards):
|
||
"""Formate l'output en texte lisible."""
|
||
output = []
|
||
output.append("=" * 60)
|
||
output.append(f"📚 CARTES SÉLECTIONNÉES - {datetime.now().strftime('%d/%m/%Y')}")
|
||
output.append("=" * 60)
|
||
output.append("")
|
||
|
||
for i, card in enumerate(cards, 1):
|
||
output.append(f"🎯 CARTE {i}/3")
|
||
output.append(f"ID: {card['id']}")
|
||
output.append(f"Fichier: {card['file']}")
|
||
output.append(f"Difficulté: {card['difficulty']} | Critique: {'⚠️ OUI' if card['critical'] else 'Non'}")
|
||
output.append(f"Dernière review: {card['last_review_str']}")
|
||
output.append(f"Score priorité: {card['priority_score']}")
|
||
output.append("")
|
||
output.append(f"QUESTION:")
|
||
output.append(card['question'])
|
||
output.append("")
|
||
output.append(f"RÉPONSE ATTENDUE:")
|
||
output.append(card['answer'])
|
||
output.append("")
|
||
if card['notes']:
|
||
output.append(f"NOTES:")
|
||
output.append(card['notes'])
|
||
output.append("")
|
||
output.append("-" * 60)
|
||
output.append("")
|
||
|
||
return "\n".join(output)
|
||
|
||
|
||
def format_output_json(cards):
|
||
"""Formate l'output en JSON."""
|
||
return json.dumps({
|
||
'date': datetime.now().strftime('%d/%m/%Y'),
|
||
'cards': [
|
||
{
|
||
'id': card['id'],
|
||
'file': card['file'],
|
||
'difficulty': card['difficulty'],
|
||
'critical': card['critical'],
|
||
'last_review': card['last_review_str'],
|
||
'priority_score': card['priority_score'],
|
||
'question': card['question'],
|
||
'answer': card['answer'],
|
||
'notes': card['notes']
|
||
}
|
||
for card in cards
|
||
]
|
||
}, indent=2, ensure_ascii=False)
|
||
|
||
|
||
def main():
|
||
parser = argparse.ArgumentParser(description='Sélectionne les cartes quotidiennes')
|
||
parser.add_argument('--num-cards', type=int, default=3, help='Nombre de cartes à sélectionner')
|
||
parser.add_argument('--json', action='store_true', help='Output en JSON')
|
||
|
||
args = parser.parse_args()
|
||
|
||
try:
|
||
cards = select_daily_cards(args.num_cards)
|
||
|
||
if not cards:
|
||
print("❌ ERREUR: Aucune carte n'a pu être chargée", file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
if args.json:
|
||
print(format_output_json(cards))
|
||
else:
|
||
print(format_output_text(cards))
|
||
|
||
except KeyboardInterrupt:
|
||
print("\n⚠️ Interrompu par l'utilisateur", file=sys.stderr)
|
||
sys.exit(130)
|
||
except Exception as e:
|
||
print(f"❌ ERREUR FATALE: {e}", file=sys.stderr)
|
||
import traceback
|
||
traceback.print_exc(file=sys.stderr)
|
||
sys.exit(1)
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main()
|