diff --git a/cemantle.py b/cemantle.py index a79e756..ebd412c 100644 --- a/cemantle.py +++ b/cemantle.py @@ -15,7 +15,7 @@ first_score = float(sys.argv[2]) / 100 # Détection de la langue lang = sys.argv[3] if len(sys.argv) > 3 else "en" if lang == "fr": - TOLERANCE = 1e-4 + TOLERANCE = 1e-2 with open("french_words.txt") as f: valid_words = set(w.strip() for w in f) print("Mode français : dictionnaire français chargé.") diff --git a/cemantle_api.py b/cemantle_api.py new file mode 100644 index 0000000..4786b00 --- /dev/null +++ b/cemantle_api.py @@ -0,0 +1,170 @@ +from flask import Flask, request, jsonify, session +from gensim.models import KeyedVectors +import numpy as np +import random +import os +from flask import send_from_directory + +app = Flask(__name__) +app.secret_key = os.urandom(24) + +# Utilitaires globaux (en mémoire pour chaque session) +game_state = {} + +TOLERANCE = 1e-4 + +# Calculer la similarité cosinus entre deux vecteurs +def cos(a, b): + return float(np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))) + +def is_valid_word(word, lang, valid_words): + if lang == "fr": + return word.islower() and all(c.isalpha() for c in word) and word in valid_words and 3 <= len(word) <= 15 + else: + return word.islower() and word.isalpha() and word in valid_words and 3 <= len(word) <= 15 + +@app.route('/init', methods=['POST']) +def init_game(): + data = request.json + first_word = data['first_word'].lower() + first_score = float(data['first_score']) / 100 + lang = data.get('lang', 'en') + if lang == "fr": + tol = 1e-2 + with open("french_words.txt") as f: + valid_words = set(w.strip() for w in f) + model_path = "frWac_non_lem_no_postag_no_phrase_200_cbow_cut0.bin" + model = KeyedVectors.load_word2vec_format(model_path, binary=True) + else: + tol = TOLERANCE + with open("english_words.txt") as f: + valid_words = set(w.strip() for w in f) + model_path = "google_news_reduced.kv" + model = KeyedVectors.load(model_path) + if first_word not in model: + return jsonify({'error': 'Mot inconnu du modèle'}), 400 + vec1 = model[first_word] + all_words = [w for w in model.index_to_key if w != first_word and is_valid_word(w, lang, valid_words)] + words = [w for w in all_words if abs(cos(vec1, model[w]) - first_score) < tol] + word_indexes = [all_words.index(w) for w in words] + session['lang'] = lang + session['first_word'] = first_word + session['tol'] = tol + session['model_path'] = model_path + session['word_indexes'] = word_indexes + session['history'] = [] # Liste des {word, score/pass, remaining} + return jsonify({'count': len(words), 'history': session['history']}) + +@app.route('/next', methods=['POST']) +def next_word(): + word_indexes = session.get('word_indexes', []) + if not word_indexes: + return jsonify({'error': 'Plus de mots'}), 400 + # Recalculer all_words à partir du modèle et du mot de départ + lang = session.get('lang', 'en') + model_path = session.get('model_path') + first_word = session.get('first_word') + if model_path.endswith('.kv'): + model = KeyedVectors.load(model_path) + else: + model = KeyedVectors.load_word2vec_format(model_path, binary=True) + if lang == 'fr': + with open("french_words.txt") as f: + valid_words = set(w.strip() for w in f) + else: + with open("english_words.txt") as f: + valid_words = set(w.strip() for w in f) + all_words = [w for w in model.index_to_key if w != first_word and is_valid_word(w, lang, valid_words)] + idx = random.choice(word_indexes) + guess = all_words[idx] + session['current_guess_idx'] = idx + return jsonify({'guess': guess, 'remaining': len(word_indexes), 'history': session.get('history', [])}) + +@app.route('/score', methods=['POST']) +def submit_score(): + data = request.json + score = data.get('score') + if score is None: + return jsonify({'error': 'Score manquant'}), 400 + try: + score = float(score) / 100 + except ValueError: + return jsonify({'error': 'Score invalide'}), 400 + lang = session.get('lang', 'en') + tol = session.get('tol', TOLERANCE) + model_path = session.get('model_path') + word_indexes = session.get('word_indexes', []) + current_guess_idx = session.get('current_guess_idx') + if current_guess_idx is None: + return jsonify({'error': 'Aucun mot courant'}), 400 + if model_path.endswith('.kv'): + model = KeyedVectors.load(model_path) + else: + model = KeyedVectors.load_word2vec_format(model_path, binary=True) + first_word = session.get('first_word') + if lang == 'fr': + with open("french_words.txt") as f: + valid_words = set(w.strip() for w in f) + else: + with open("english_words.txt") as f: + valid_words = set(w.strip() for w in f) + all_words = [w for w in model.index_to_key if w != first_word and is_valid_word(w, lang, valid_words)] + guess_word = all_words[current_guess_idx] + vec_guess = model[guess_word] + new_word_indexes = [i for i in word_indexes if abs(cos(vec_guess, model[all_words[i]]) - score) < tol] + session['word_indexes'] = new_word_indexes + history = session.get('history', []) + history.append({'word': guess_word, 'score': score, 'remaining': len(new_word_indexes)}) + session['history'] = history + if not new_word_indexes: + return jsonify({'result': 'Plus aucun mot possible. Terminé.', 'history': history}) + if len(new_word_indexes) == 1: + return jsonify({'result': f"Plus qu'un seul mot possible: {all_words[new_word_indexes[0]]}", 'history': history}) + return jsonify({'remaining': len(new_word_indexes), 'history': history}) + +@app.route('/skip', methods=['POST']) +def skip_word(): + word_indexes = session.get('word_indexes', []) + current_guess_idx = session.get('current_guess_idx') + if current_guess_idx is None or not word_indexes: + return jsonify({'error': 'Aucun mot à passer'}), 400 + # Recalculer all_words à partir du modèle et du mot de départ + lang = session.get('lang', 'en') + model_path = session.get('model_path') + first_word = session.get('first_word') + if model_path.endswith('.kv'): + model = KeyedVectors.load(model_path) + else: + model = KeyedVectors.load_word2vec_format(model_path, binary=True) + if lang == 'fr': + with open("french_words.txt") as f: + valid_words = set(w.strip() for w in f) + else: + with open("english_words.txt") as f: + valid_words = set(w.strip() for w in f) + all_words = [w for w in model.index_to_key if w != first_word and is_valid_word(w, lang, valid_words)] + try: + word_indexes.remove(current_guess_idx) + except ValueError: + pass + session['word_indexes'] = word_indexes + history = session.get('history', []) + history.append({'word': all_words[current_guess_idx], 'score': 'pass', 'remaining': len(word_indexes)}) + session['history'] = history + if not word_indexes: + return jsonify({'result': 'Plus aucun mot possible. Terminé.', 'history': history}) + return jsonify({'remaining': len(word_indexes), 'history': history}) + + +# Servir index.html à la racine +@app.route('/') +def root(): + return send_from_directory('.', 'index.html') + +# Servir les fichiers statiques (ex: .txt, .js, .css) +@app.route('/') +def static_files(filename): + return send_from_directory('.', filename) + +if __name__ == '__main__': + app.run(debug=True) diff --git a/google_news_reduced.kv b/google_news_reduced.kv new file mode 100644 index 0000000..92d3f18 Binary files /dev/null and b/google_news_reduced.kv differ diff --git a/google_news_reduced.kv.vectors.npy b/google_news_reduced.kv.vectors.npy new file mode 100644 index 0000000..f3b6f0d Binary files /dev/null and b/google_news_reduced.kv.vectors.npy differ diff --git a/index.html b/index.html index e69de29..b9a0efb 100644 --- a/index.html +++ b/index.html @@ -0,0 +1,153 @@ + + + + + + Cemantle Solver Web + + + +
+

Cemantle Solver

+
+ + + + +
+ +
+ + +