couple-repo/anki_tingting/scripts/get_daily_cards.py
StillHammer b243059eeb Restore daily check system decentralized architecture
- 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>
2025-11-21 19:32:14 +08:00

303 lines
9.7 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()