transformed into API
This commit is contained in:
parent
0b2f99b70b
commit
8182bc0eb9
5 changed files with 324 additions and 1 deletions
|
|
@ -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é.")
|
||||
|
|
|
|||
170
cemantle_api.py
Normal file
170
cemantle_api.py
Normal file
|
|
@ -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('/<path:filename>')
|
||||
def static_files(filename):
|
||||
return send_from_directory('.', filename)
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run(debug=True)
|
||||
BIN
google_news_reduced.kv
Normal file
BIN
google_news_reduced.kv
Normal file
Binary file not shown.
BIN
google_news_reduced.kv.vectors.npy
Normal file
BIN
google_news_reduced.kv.vectors.npy
Normal file
Binary file not shown.
153
index.html
153
index.html
|
|
@ -0,0 +1,153 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Cemantle Solver Web</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; margin: 2em; background: #f7f7f7; }
|
||||
#container { background: #fff; padding: 2em; border-radius: 8px; box-shadow: 0 2px 8px #0001; max-width: 650px; margin: auto; }
|
||||
#word { font-size: 2em; margin: 1em 0; }
|
||||
#scoreInput { font-size: 1.2em; width: 100px; }
|
||||
button { font-size: 1em; padding: 0.5em 1em; margin-top: 1em; }
|
||||
#result { margin-top: 1em; color: green; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="container">
|
||||
<h2>Cemantle Solver</h2>
|
||||
<div id="setup">
|
||||
<label>Mot de départ : <input id="firstWord" type="text" /></label>
|
||||
<label>Score de départ : <input id="firstScore" type="number" min="0" max="1000" /></label>
|
||||
<button id="startBtn">Démarrer</button>
|
||||
<span id="loading" style="display:none; margin-left:1em; color:#0077cc;">Chargement…</span>
|
||||
</div>
|
||||
<div id="game" style="display:none;">
|
||||
<div id="word"></div>
|
||||
<label for="scoreInput">Score :</label>
|
||||
<input type="number" id="scoreInput" min="0" max="1000" />
|
||||
<button id="submitScoreBtn">Valider</button>
|
||||
<button id="skipWordBtn" style="margin-left:1em;">Passer</button>
|
||||
<div id="result"></div>
|
||||
<div id="remaining"></div>
|
||||
<h3>Historique</h3>
|
||||
<ul id="history"></ul>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
let currentWord = '';
|
||||
let history = [];
|
||||
let remaining = 0;
|
||||
|
||||
function updateHistory() {
|
||||
const histElem = document.getElementById('history');
|
||||
histElem.innerHTML = '';
|
||||
history.forEach(item => {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = `${item.word} : ${item.score} (restants: ${item.remaining})`;
|
||||
histElem.appendChild(li);
|
||||
});
|
||||
}
|
||||
|
||||
function updateRemaining() {
|
||||
document.getElementById('remaining').textContent = `Mots restants : ${remaining}`;
|
||||
}
|
||||
|
||||
async function startGame() {
|
||||
const firstWord = document.getElementById('firstWord').value;
|
||||
const firstScore = document.getElementById('firstScore').value;
|
||||
if (!firstWord || !firstScore) {
|
||||
alert('Remplis le mot et le score de départ');
|
||||
return;
|
||||
}
|
||||
document.getElementById('loading').style.display = '';
|
||||
try {
|
||||
const resp = await fetch('/init', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ first_word: firstWord, first_score: firstScore })
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.error) {
|
||||
alert(data.error);
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
return;
|
||||
}
|
||||
history = data.history || [];
|
||||
remaining = data.count;
|
||||
document.getElementById('setup').style.display = 'none';
|
||||
document.getElementById('game').style.display = '';
|
||||
await nextWord();
|
||||
} catch (e) {
|
||||
alert('Erreur lors du chargement : ' + e);
|
||||
} finally {
|
||||
document.getElementById('loading').style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
async function nextWord() {
|
||||
const resp = await fetch('/next', { method: 'POST' });
|
||||
const data = await resp.json();
|
||||
if (data.error) {
|
||||
document.getElementById('word').textContent = 'Fini !';
|
||||
document.getElementById('submitScoreBtn').disabled = true;
|
||||
document.getElementById('skipWordBtn').disabled = true;
|
||||
return;
|
||||
}
|
||||
currentWord = data.guess;
|
||||
remaining = data.remaining;
|
||||
history = data.history || [];
|
||||
document.getElementById('word').textContent = currentWord;
|
||||
document.getElementById('scoreInput').value = '';
|
||||
document.getElementById('result').textContent = '';
|
||||
document.getElementById('scoreInput').focus();
|
||||
updateHistory();
|
||||
updateRemaining();
|
||||
}
|
||||
|
||||
document.getElementById('startBtn').onclick = startGame;
|
||||
document.getElementById('submitScoreBtn').onclick = async function() {
|
||||
const score = document.getElementById('scoreInput').value;
|
||||
if (score === '') {
|
||||
document.getElementById('result').textContent = 'Veuillez entrer un score.';
|
||||
return;
|
||||
}
|
||||
const resp = await fetch('/score', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ score })
|
||||
});
|
||||
const data = await resp.json();
|
||||
if (data.error) {
|
||||
document.getElementById('result').textContent = data.error;
|
||||
return;
|
||||
}
|
||||
if (data.result) {
|
||||
document.getElementById('result').textContent = data.result;
|
||||
}
|
||||
history = data.history || history;
|
||||
remaining = data.remaining || remaining;
|
||||
updateHistory();
|
||||
updateRemaining();
|
||||
await nextWord();
|
||||
};
|
||||
|
||||
document.getElementById('skipWordBtn').onclick = async function() {
|
||||
const resp = await fetch('/skip', { method: 'POST' });
|
||||
const data = await resp.json();
|
||||
if (data.error) {
|
||||
document.getElementById('result').textContent = data.error;
|
||||
return;
|
||||
}
|
||||
if (data.result) {
|
||||
document.getElementById('result').textContent = data.result;
|
||||
}
|
||||
history = data.history || history;
|
||||
remaining = data.remaining || remaining;
|
||||
updateHistory();
|
||||
updateRemaining();
|
||||
await nextWord();
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Loading…
Reference in a new issue