- Add CARD_SYSTEM_GUIDE.md: Complete documentation for card creation, management, and workflow - Add emotional_calibration.md card (COMM-001): New critical card for managing emotions during conflicts - Add Python automation scripts: - get_daily_cards.py: Intelligent card selection based on priority scoring - log_session.py: Automated session logging and stats updates - Update CLAUDE.md: New protocol using automated scripts instead of manual selection - Update card_database.md: Restructured with cleaner table format + new COMM category - Update all card review history: Automated stats from recent sessions - Update daily_sessions.md: Add automated session logs (3 new sessions from 2025-11-19) System improvements: - Centralized card database with individual card files - Automated priority calculation (Critical, Times Failed, Never reviewed, Overdue) - Full stats automation (no more manual updates) - Better spaced repetition logic 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
258 lines
7.7 KiB
Python
258 lines
7.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 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."""
|
|
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:
|
|
raise ValueError("Impossible de trouver la table dans card_database.md")
|
|
|
|
table_rows = match.group(1).strip().split('\n')
|
|
|
|
cards = []
|
|
for row in table_rows:
|
|
# 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:
|
|
continue
|
|
|
|
card_id, card_file, difficulty, frequency, last_review, success_rate, times_failed, critical = cols
|
|
|
|
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 == '⚠️'
|
|
})
|
|
|
|
return cards
|
|
|
|
|
|
def load_card_content(card_file):
|
|
"""Charge le contenu d'une carte depuis son fichier."""
|
|
card_path = CARDS_DIR / card_file
|
|
|
|
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
|
|
}
|
|
|
|
|
|
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
|
|
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 args.json:
|
|
print(format_output_json(cards))
|
|
else:
|
|
print(format_output_text(cards))
|
|
|
|
except Exception as e:
|
|
print(f"❌ ERREUR: {e}", file=sys.stderr)
|
|
import sys
|
|
sys.exit(1)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
import sys
|
|
main()
|