couple-repo/anki_tingting/scripts/get_daily_cards.py
StillHammer bdbe17a3a0 Enhance daily check system with automation and new cards
- 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>
2025-11-19 11:16:26 +08:00

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()