wchess : whisper assisted chess (#1595)
* wchess: whisper assisted chess * wchess: fix allowed moves in check * wchess: touchstart, touchend events * wchess: css, disabled button * wchess : html touches * wchess : minor fixes and code style * wchess : bump encoder context to 1280 * wchess : index.html * wchess : fix CI warnings * wchess : add array header * wchess : build static library * wchess : display grammar * wchess : update UX * wchess : add comment * wchess : add README --------- Co-authored-by: Georgi Gerganov <ggerganov@gmail.com>
@ -770,6 +770,7 @@ Some of the examples are even ported to run in the browser using WebAssembly. Ch
|
||||
| [bench](examples/bench) | [bench.wasm](examples/bench.wasm) | Benchmark the performance of Whisper on your machine |
|
||||
| [stream](examples/stream) | [stream.wasm](examples/stream.wasm) | Real-time transcription of raw microphone capture |
|
||||
| [command](examples/command) | [command.wasm](examples/command.wasm) | Basic voice assistant example for receiving voice commands from the mic |
|
||||
| [wchess](examples/wchess) | [wchess.wasm](examples/wchess) | Voice-controlled chess |
|
||||
| [talk](examples/talk) | [talk.wasm](examples/talk.wasm) | Talk with a GPT-2 bot |
|
||||
| [talk-llama](examples/talk-llama) | | Talk with a LLaMA bot |
|
||||
| [whisper.objc](examples/whisper.objc) | | iOS mobile application using whisper.cpp |
|
||||
|
@ -73,3 +73,5 @@ else()
|
||||
add_subdirectory(talk-llama)
|
||||
add_subdirectory(lsp)
|
||||
endif()
|
||||
|
||||
add_subdirectory(wchess)
|
||||
|
@ -22,6 +22,7 @@ var printTextarea = (function() {
|
||||
async function clearCache() {
|
||||
if (confirm('Are you sure you want to clear the cache?\nAll the models will be downloaded again.')) {
|
||||
indexedDB.deleteDatabase(dbName);
|
||||
location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
|
9
examples/wchess/CMakeLists.txt
Normal file
@ -0,0 +1,9 @@
|
||||
set(CMAKE_CXX_STANDARD 11)
|
||||
|
||||
add_subdirectory(libwchess)
|
||||
|
||||
if (EMSCRIPTEN)
|
||||
add_subdirectory(wchess.wasm)
|
||||
else()
|
||||
add_subdirectory(wchess.cmd)
|
||||
endif()
|
5
examples/wchess/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# wchess.wasm
|
||||
|
||||
Voice-controlled chess using Whisper + WebAssembly
|
||||
|
||||
Online demo: https://whisper.ggerganov.com/wchess/
|
19
examples/wchess/libwchess/CMakeLists.txt
Normal file
@ -0,0 +1,19 @@
|
||||
add_library(wchess-core STATIC
|
||||
WChess.cpp
|
||||
WChess.h
|
||||
Chessboard.cpp
|
||||
Chessboard.h
|
||||
)
|
||||
|
||||
target_link_libraries(wchess-core
|
||||
PUBLIC
|
||||
whisper
|
||||
common
|
||||
)
|
||||
|
||||
target_include_directories(wchess-core
|
||||
PUBLIC
|
||||
"$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>"
|
||||
)
|
||||
|
||||
# add_executable(test-chessboard test-chessboard.cpp Chessboard.cpp)
|
803
examples/wchess/libwchess/Chessboard.cpp
Normal file
@ -0,0 +1,803 @@
|
||||
#include "Chessboard.h"
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <set>
|
||||
#include <list>
|
||||
#include <chrono>
|
||||
|
||||
namespace {
|
||||
constexpr std::array<const char*, 64> positions = {
|
||||
"a1", "b1", "c1", "d1", "e1", "f1", "g1", "h1",
|
||||
"a2", "b2", "c2", "d2", "e2", "f2", "g2", "h2",
|
||||
"a3", "b3", "c3", "d3", "e3", "f3", "g3", "h3",
|
||||
"a4", "b4", "c4", "d4", "e4", "f4", "g4", "h4",
|
||||
"a5", "b5", "c5", "d5", "e5", "f5", "g5", "h5",
|
||||
"a6", "b6", "c6", "d6", "e6", "f6", "g6", "h6",
|
||||
"a7", "b7", "c7", "d7", "e7", "f7", "g7", "h7",
|
||||
"a8", "b8", "c8", "d8", "e8", "f8", "g8", "h8",
|
||||
};
|
||||
constexpr char INVALID_POS = positions.size();
|
||||
constexpr int R = 0; // rank index
|
||||
constexpr int F = 1; // file index
|
||||
#define FILE (c[F] - '1')
|
||||
#define RANK (c[R] - 'a')
|
||||
constexpr char operator ""_P(const char * c, size_t size) {
|
||||
return size < 2 || RANK < 0 || RANK > 7 ||
|
||||
FILE < 0 || FILE > 7 ? INVALID_POS : FILE * 8 + RANK;
|
||||
}
|
||||
#undef FILE
|
||||
#undef RANK
|
||||
|
||||
struct sview {
|
||||
const char * ptr = nullptr;
|
||||
size_t size = 0;
|
||||
|
||||
sview() = default;
|
||||
sview(const char * p, size_t s) : ptr(p), size(s) {}
|
||||
sview(const std::string& s) : ptr(s.data()), size(s.size()) {}
|
||||
|
||||
size_t find(char del, size_t pos) {
|
||||
while (pos < size && ptr[pos] != del) ++pos;
|
||||
return pos < size ? pos : std::string::npos;
|
||||
}
|
||||
};
|
||||
|
||||
std::vector<sview> split(sview str, char del) {
|
||||
std::vector<sview> res;
|
||||
size_t cur = 0;
|
||||
size_t last = 0;
|
||||
while (cur != std::string::npos) {
|
||||
if (str.ptr[last] == ' ') {
|
||||
++last;
|
||||
continue;
|
||||
}
|
||||
cur = str.find(del, last);
|
||||
size_t len = cur == std::string::npos ? str.size - last : cur - last;
|
||||
res.emplace_back(str.ptr + last, len);
|
||||
last = cur + 1;
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
char strToPos(sview str) {
|
||||
return operator ""_P(str.ptr, str.size);
|
||||
}
|
||||
|
||||
constexpr std::array<const char*, 6> pieceNames = {
|
||||
"pawn", "knight", "bishop", "rook", "queen", "king",
|
||||
};
|
||||
|
||||
static constexpr std::array<char, 6> blackShort = {
|
||||
'p', 'n', 'b', 'r', 'q', 'k',
|
||||
};
|
||||
static constexpr std::array<char, 6> whiteShort = {
|
||||
'P', 'N', 'B', 'R', 'Q', 'K',
|
||||
};
|
||||
|
||||
char strToType(sview str) {
|
||||
auto it = std::find_if(pieceNames.begin(), pieceNames.end(), [str] (const char* name) { return strncmp(name, str.ptr, str.size) == 0; });
|
||||
return it != pieceNames.end() ? it - pieceNames.begin() : pieceNames.size();
|
||||
}
|
||||
|
||||
// directions
|
||||
using Direction = std::array<char, 2>;
|
||||
|
||||
constexpr Direction N = {(char) 0, (char) 1};
|
||||
constexpr Direction NNE = {(char) 1, (char) 2};
|
||||
constexpr Direction NE = {(char) 1, (char) 1};
|
||||
constexpr Direction ENE = {(char) 2, (char) 1};
|
||||
constexpr Direction E = {(char) 1, (char) 0};
|
||||
constexpr Direction ESE = {(char) 2, (char) -1};
|
||||
constexpr Direction SE = {(char) 1, (char) -1};
|
||||
constexpr Direction SSE = {(char) 1, (char) -2};
|
||||
constexpr Direction S = {(char) 0, (char) -1};
|
||||
constexpr Direction SSW = {(char) -1, (char) -2};
|
||||
constexpr Direction SW = {(char) -1, (char) -1};
|
||||
constexpr Direction WSW = {(char) -2, (char) -1};
|
||||
constexpr Direction W = {(char) -1, (char) 0};
|
||||
constexpr Direction WNW = {(char) -2, (char) 1};
|
||||
constexpr Direction NW = {(char) -1, (char) 1};
|
||||
constexpr Direction NNW = {(char) -1, (char) 2};
|
||||
|
||||
char makeStep(char pos, const Direction& d) {
|
||||
char next[2] = { char(positions[pos][R] + d[R]) , char(positions[pos][F] + d[F]) };
|
||||
return strToPos(sview{next, sizeof(next)});
|
||||
}
|
||||
|
||||
template<class Modifier>
|
||||
char traverse(char pos, const Direction& d, const Modifier& m, int count = 8) {
|
||||
while (--count >= 0) {
|
||||
pos = makeStep(pos, d);
|
||||
if (pos == INVALID_POS || m(pos)) break;
|
||||
}
|
||||
return pos;
|
||||
}
|
||||
|
||||
Direction normalize(const Direction& distance) {
|
||||
//return {char((distance[R] > 0) - (distance[R] < 0)), char((distance[F] > 0) - (distance[F] < 0))};
|
||||
const int drp = distance[R] > 0 ? 1 : 0;
|
||||
const int drn = distance[R] < 0 ? 1 : 0;
|
||||
const int dfp = distance[F] > 0 ? 1 : 0;
|
||||
const int dfn = distance[F] < 0 ? 1 : 0;
|
||||
return {char(drp - drn), char(dfp - dfn)};
|
||||
}
|
||||
|
||||
struct Pin {
|
||||
Direction d;
|
||||
Piece* pinner;
|
||||
Piece* pinned;
|
||||
};
|
||||
using Pins = std::list<Pin>;
|
||||
using Board = std::array<Piece*, 64>;
|
||||
|
||||
std::vector<Direction> filter(const Direction& pin, std::initializer_list<Direction> directions) {
|
||||
if (pin[R] == 0 && pin[F] == 0) return directions;
|
||||
std::vector<Direction> result;
|
||||
for (auto& d : directions) {
|
||||
if ((d[R] == pin[R] || d[R] == -pin[R]) && (d[F] == pin[F] || d[F] == -pin[F])) result.push_back(d);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class Piece {
|
||||
public:
|
||||
enum Types : char {
|
||||
Pawn,
|
||||
Knight,
|
||||
Bishop,
|
||||
Rook,
|
||||
Queen,
|
||||
King,
|
||||
//
|
||||
NUM_PIECES
|
||||
};
|
||||
|
||||
enum Colors : char {
|
||||
White,
|
||||
Black,
|
||||
};
|
||||
|
||||
const char* name() const;
|
||||
char initial() const;
|
||||
Types type() const { return m_type; }
|
||||
Colors color() const { return m_color; }
|
||||
char pos() const { return m_pos; }
|
||||
void setPos(char pos) {
|
||||
m_pos = pos;
|
||||
invalidate();
|
||||
}
|
||||
const char* coord() const;
|
||||
const std::set<char>& allowed() const { return m_allowed; }
|
||||
bool canReach(char pos) const;
|
||||
virtual bool movePattern(char pos) const = 0;
|
||||
void take();
|
||||
virtual void reinit(const State& state) = 0;
|
||||
void invalidate();
|
||||
protected:
|
||||
Piece(Types type, Colors color, char pos, std::set<char> allowed)
|
||||
: m_type(type), m_color(color), m_pos(pos), m_allowed(std::move(allowed)) {}
|
||||
Piece(const Piece&) = delete;
|
||||
~Piece() = default;
|
||||
|
||||
const Types m_type;
|
||||
const Colors m_color;
|
||||
char m_pos;
|
||||
std::set<char> m_allowed;
|
||||
bool m_update = false;
|
||||
};
|
||||
|
||||
struct Pawn : public Piece {
|
||||
Pawn(Colors color, char pos, std::set<char> next) : Piece(Types::Pawn, color, pos, std::move(next)) {}
|
||||
|
||||
bool is_first_move() const {
|
||||
return m_color ? coord()[F] == '7' : coord()[F] == '2';
|
||||
}
|
||||
|
||||
virtual bool movePattern(char pos) const override {
|
||||
if (m_pos == INVALID_POS) return false;
|
||||
auto cur = coord();
|
||||
auto next = positions[pos];
|
||||
Direction distance = {char(next[R] - cur[R]), char(next[F] - cur[F])};
|
||||
char forward = m_color ? -1 : 1;
|
||||
return (forward == distance[F] && distance[R] * distance[R] <= 1)
|
||||
|| (is_first_move() && 2 * forward == distance[F] && distance[R] == 0);
|
||||
}
|
||||
|
||||
virtual void reinit(const State& state) override;
|
||||
};
|
||||
|
||||
struct Knight : public Piece {
|
||||
Knight(Colors color, char pos, std::set<char> next) : Piece(Types::Knight, color, pos, std::move(next)) {}
|
||||
|
||||
virtual bool movePattern(char pos) const override {
|
||||
if (m_pos == INVALID_POS) return false;
|
||||
auto cur = coord();
|
||||
auto next = positions[pos];
|
||||
Direction diff = {char(next[R] - cur[R]), char(next[F] - cur[F])};
|
||||
return diff[R]*diff[R] + diff[F]*diff[F] == 5;
|
||||
}
|
||||
|
||||
virtual void reinit(const State& state) override;
|
||||
};
|
||||
|
||||
struct Bishop : public Piece {
|
||||
Bishop(Colors color, char pos) : Piece(Types::Bishop, color, pos, {}) {}
|
||||
|
||||
virtual bool movePattern(char pos) const override {
|
||||
if (m_pos == INVALID_POS) return false;
|
||||
auto cur = coord();
|
||||
auto next = positions[pos];
|
||||
return cur[R] - cur[F] == next[R] - next[F] || cur[R] + cur[F] == next[R] + next[F];
|
||||
}
|
||||
|
||||
virtual void reinit(const State& state) override;
|
||||
};
|
||||
|
||||
struct Rook : public Piece {
|
||||
Rook(Colors color, char pos) : Piece(Types::Rook, color, pos, {}) {}
|
||||
|
||||
virtual bool movePattern(char pos) const override {
|
||||
if (m_pos == INVALID_POS) return false;
|
||||
auto cur = coord();
|
||||
auto next = positions[pos];
|
||||
return cur[R] == next[R] || cur[F] == next[F];
|
||||
}
|
||||
|
||||
virtual void reinit(const State& state) override;
|
||||
};
|
||||
|
||||
struct Queen : public Piece {
|
||||
Queen(Colors color, char pos) : Piece(Types::Queen, color, pos, {}) {}
|
||||
|
||||
virtual bool movePattern(char pos) const override {
|
||||
if (m_pos == INVALID_POS) return false;
|
||||
auto cur = coord();
|
||||
auto next = positions[pos];
|
||||
return cur[R] == next[R] || cur[F] == next[F] || cur[R] - cur[F] == next[R] - next[F] || cur[R] + cur[F] == next[R] + next[F];
|
||||
}
|
||||
|
||||
virtual void reinit(const State& state) override;
|
||||
};
|
||||
|
||||
struct King : public Piece {
|
||||
King(Colors color, char pos) : Piece(Types::King, color, pos, {}) {}
|
||||
|
||||
virtual bool movePattern(char pos) const override {
|
||||
if (m_pos == INVALID_POS) return false;
|
||||
auto cur = coord();
|
||||
auto next = positions[pos];
|
||||
Direction diff = {char(next[R] - cur[R]), char(next[F] - cur[F])};
|
||||
return diff[R]*diff[R] + diff[F]*diff[F] <= 2;
|
||||
}
|
||||
|
||||
virtual void reinit(const State& state) override;
|
||||
};
|
||||
|
||||
struct PieceSet {
|
||||
Piece* begin() { return &p1; }
|
||||
Piece* end() { return &r2 + 1; }
|
||||
const Piece* begin() const { return &p1; }
|
||||
const Piece* end() const { return &r2 + 1; }
|
||||
Piece& operator[](int i) { return *(begin() + i); }
|
||||
const Piece& operator[](int i) const { return *(begin() + i); }
|
||||
|
||||
Pawn p1;
|
||||
Pawn p2;
|
||||
Pawn p3;
|
||||
Pawn p4;
|
||||
Pawn p5;
|
||||
Pawn p6;
|
||||
Pawn p7;
|
||||
Pawn p8;
|
||||
Rook r1;
|
||||
Knight n1;
|
||||
Bishop b1;
|
||||
Queen q;
|
||||
King k;
|
||||
Bishop b2;
|
||||
Knight n2;
|
||||
Rook r2;
|
||||
};
|
||||
|
||||
struct State {
|
||||
State();
|
||||
PieceSet blacks;
|
||||
PieceSet whites;
|
||||
Board board;
|
||||
Pins blackPins;
|
||||
Pins whitePins;
|
||||
};
|
||||
|
||||
Direction findPin(const Piece& piece, const State& state) {
|
||||
auto& pins = piece.color() ? state.blackPins : state.whitePins;
|
||||
auto it = std::find_if(pins.begin(), pins.end(), [&] (const Pin& pin) { return pin.pinned == &piece; });
|
||||
if (it != pins.end()) return it->d;
|
||||
return {0, 0};
|
||||
}
|
||||
|
||||
struct Find {
|
||||
Find(const Board& board) : m_board(board) {}
|
||||
bool operator() (char pos) const { return m_board[pos]; }
|
||||
const Board& m_board;
|
||||
};
|
||||
|
||||
struct Add {
|
||||
Add(const Board& board, std::set<char>& moves, Piece::Colors color) : m_board(board), m_moves(moves), m_color(color) {}
|
||||
bool operator() (char pos) const {
|
||||
if (!m_board[pos] || m_board[pos]->color() != m_color) m_moves.insert(pos);
|
||||
return m_board[pos];
|
||||
}
|
||||
const Board& m_board;
|
||||
std::set<char>& m_moves;
|
||||
Piece::Colors m_color;
|
||||
};
|
||||
|
||||
void Pawn::reinit(const State& state) {
|
||||
if (m_pos == INVALID_POS) return;
|
||||
if (!m_update) return;
|
||||
m_update = false;
|
||||
m_allowed.clear();
|
||||
|
||||
auto pin = findPin(*this, state);
|
||||
|
||||
auto & left = m_color ? SW : NW;
|
||||
auto & right = m_color ? SE : NE;
|
||||
|
||||
for (auto& direction : filter(pin, { left, right })) {
|
||||
auto pos = makeStep(m_pos, direction);
|
||||
if (pos != INVALID_POS && state.board[pos] && state.board[pos]->color() != m_color) m_allowed.insert(pos);
|
||||
}
|
||||
|
||||
auto & forward = m_color ? S : N;
|
||||
if (!filter(pin, {forward}).empty()) {
|
||||
traverse(m_pos, forward, [&] (char pos) {
|
||||
if (!state.board[pos]) m_allowed.insert(pos);
|
||||
return state.board[pos] || !is_first_move();
|
||||
}, 2);
|
||||
}
|
||||
}
|
||||
|
||||
void Knight::reinit(const State& state) {
|
||||
if (m_pos == INVALID_POS) return;
|
||||
if (!m_update) return;
|
||||
m_update = false;
|
||||
m_allowed.clear();
|
||||
auto pin = findPin(*this, state);
|
||||
if (pin[R] != 0 || pin[F] != 0) return;
|
||||
for (auto& direction : { NNE, ENE, ESE, SSE, SSW, WSW, WNW, NNW }) {
|
||||
auto pos = makeStep(m_pos, direction);
|
||||
if (pos != INVALID_POS && (!state.board[pos] || state.board[pos]->color() != m_color)) m_allowed.insert(pos);
|
||||
}
|
||||
}
|
||||
|
||||
void Bishop::reinit(const State& state) {
|
||||
if (m_pos == INVALID_POS) return;
|
||||
if (!m_update) return;
|
||||
m_update = false;
|
||||
m_allowed.clear();
|
||||
auto pin = findPin(*this, state);
|
||||
for (auto& direction : filter(pin, { NE, SE, SW, NW })) {
|
||||
traverse(m_pos, direction, Add(state.board, m_allowed, m_color));
|
||||
}
|
||||
}
|
||||
|
||||
void Rook::reinit(const State& state) {
|
||||
if (m_pos == INVALID_POS) return;
|
||||
if (!m_update) return;
|
||||
m_update = false;
|
||||
m_allowed.clear();
|
||||
auto pin = findPin(*this, state);
|
||||
for (auto& direction : filter(pin, { N, E, S, W })) {
|
||||
traverse(m_pos, direction, Add(state.board, m_allowed, m_color));
|
||||
}
|
||||
}
|
||||
|
||||
void Queen::reinit(const State& state) {
|
||||
if (m_pos == INVALID_POS) return;
|
||||
if (!m_update) return;
|
||||
m_update = false;
|
||||
m_allowed.clear();
|
||||
auto pin = findPin(*this, state);
|
||||
for (auto& direction : filter(pin, { N, NE, E, SE, S, SW, W, NW })) {
|
||||
traverse(m_pos, direction, Add(state.board, m_allowed, m_color));
|
||||
}
|
||||
}
|
||||
|
||||
void King::reinit(const State& state) {
|
||||
if (m_pos == INVALID_POS) return;
|
||||
if (!m_update) return;
|
||||
m_update = false;
|
||||
m_allowed.clear();
|
||||
auto& enemyPieces = m_color ? state.whites : state.blacks;
|
||||
auto& pawnAttackLeft = m_color ? SW : NW;
|
||||
auto& pawnAttackRight = m_color ? SE : NE;
|
||||
for (auto& direction : { N, NE, E, SE, S, SW, W, NW }) {
|
||||
auto pos = makeStep(m_pos, direction);
|
||||
bool accept = pos != INVALID_POS && !(state.board[pos] && state.board[pos]->color() == m_color);
|
||||
if (accept) {
|
||||
for (auto& p : enemyPieces) {
|
||||
if (!p.movePattern(pos)) continue;
|
||||
if (p.type() == Piece::Knight || p.type() == Piece::King) {
|
||||
accept = false;
|
||||
break;
|
||||
}
|
||||
else if (p.type() == Piece::Pawn) {
|
||||
auto from = positions[pos];
|
||||
auto to = p.coord();
|
||||
Direction d {char(to[R] - from[R]), char(to[F] - from[F])};
|
||||
if (d == pawnAttackLeft || d == pawnAttackRight) {
|
||||
accept = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
else {
|
||||
auto from = positions[pos];
|
||||
auto to = p.coord();
|
||||
Direction d = normalize({char(to[R] - from[R]), char(to[F] - from[F])});
|
||||
auto reached = traverse(pos, d, Find(state.board));
|
||||
if (p.pos() == reached) {
|
||||
accept = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (accept) m_allowed.insert(pos);
|
||||
}
|
||||
}
|
||||
|
||||
const char* Piece::name() const {
|
||||
static_assert(pieceNames.size() == Piece::NUM_PIECES, "Mismatch between piece names and types");
|
||||
return pieceNames[m_type];
|
||||
}
|
||||
|
||||
char Piece::initial() const {
|
||||
static_assert(blackShort.size() == Piece::NUM_PIECES, "Mismatch between piece names and types");
|
||||
static_assert(whiteShort.size() == Piece::NUM_PIECES, "Mismatch between piece names and types");
|
||||
return m_color ? blackShort[m_type] : whiteShort[m_type];
|
||||
}
|
||||
|
||||
void Piece::invalidate() {
|
||||
m_update = true;
|
||||
}
|
||||
|
||||
|
||||
const char* Piece::coord() const {
|
||||
if (m_pos == INVALID_POS) return "";
|
||||
return positions[m_pos];
|
||||
}
|
||||
|
||||
bool Piece::canReach(char pos) const {
|
||||
return movePattern(pos) && m_allowed.count(pos);
|
||||
}
|
||||
|
||||
void Piece::take() {
|
||||
m_pos = INVALID_POS;
|
||||
m_allowed = {};
|
||||
}
|
||||
|
||||
State::State()
|
||||
: blacks {
|
||||
{Piece::Black, "a7"_P, {"a5"_P, "a6"_P} },
|
||||
{Piece::Black, "b7"_P, {"b5"_P, "b6"_P} },
|
||||
{Piece::Black, "c7"_P, {"c5"_P, "c6"_P} },
|
||||
{Piece::Black, "d7"_P, {"d5"_P, "d6"_P} },
|
||||
{Piece::Black, "e7"_P, {"e5"_P, "e6"_P} },
|
||||
{Piece::Black, "f7"_P, {"f5"_P, "f6"_P} },
|
||||
{Piece::Black, "g7"_P, {"g5"_P, "g6"_P} },
|
||||
{Piece::Black, "h7"_P, {"h5"_P, "h6"_P} },
|
||||
{Piece::Black, "a8"_P},
|
||||
{Piece::Black, "b8"_P, {"a6"_P, "c6"_P} },
|
||||
{Piece::Black, "c8"_P},
|
||||
{Piece::Black, "d8"_P},
|
||||
{Piece::Black, "e8"_P},
|
||||
{Piece::Black, "f8"_P},
|
||||
{Piece::Black, "g8"_P, {"f6"_P, "h6"_P} },
|
||||
{Piece::Black, "h8"_P},
|
||||
}
|
||||
, whites {
|
||||
{Piece::White, "a2"_P, {"a3"_P, "a4"_P} },
|
||||
{Piece::White, "b2"_P, {"b3"_P, "b4"_P} },
|
||||
{Piece::White, "c2"_P, {"c3"_P, "c4"_P} },
|
||||
{Piece::White, "d2"_P, {"d3"_P, "d4"_P} },
|
||||
{Piece::White, "e2"_P, {"e3"_P, "e4"_P} },
|
||||
{Piece::White, "f2"_P, {"f3"_P, "f4"_P} },
|
||||
{Piece::White, "g2"_P, {"g3"_P, "g4"_P} },
|
||||
{Piece::White, "h2"_P, {"h3"_P, "h4"_P} },
|
||||
{Piece::White, "a1"_P},
|
||||
{Piece::White, "b1"_P, {"a3"_P, "c3"_P} },
|
||||
{Piece::White, "c1"_P},
|
||||
{Piece::White, "d1"_P},
|
||||
{Piece::White, "e1"_P},
|
||||
{Piece::White, "f1"_P},
|
||||
{Piece::White, "g1"_P, {"f3"_P, "h3"_P} },
|
||||
{Piece::White, "h1"_P},
|
||||
}
|
||||
, board {{
|
||||
&whites[ 8], &whites[ 9], &whites[10], &whites[11], &whites[12], &whites[13], &whites[14], &whites[15],
|
||||
&whites[ 0], &whites[ 1], &whites[ 2], &whites[ 3], &whites[ 4], &whites[ 5], &whites[ 6], &whites[ 7],
|
||||
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
|
||||
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
|
||||
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
|
||||
nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr, nullptr,
|
||||
&blacks[ 0], &blacks[ 1], &blacks[ 2], &blacks[ 3], &blacks[ 4], &blacks[ 5], &blacks[ 6], &blacks[ 7],
|
||||
&blacks[ 8], &blacks[ 9], &blacks[10], &blacks[11], &blacks[12], &blacks[13], &blacks[14], &blacks[15],
|
||||
}}
|
||||
{}
|
||||
|
||||
Chessboard::Chessboard()
|
||||
: m_state(new State())
|
||||
{
|
||||
setGrammar();
|
||||
}
|
||||
|
||||
Chessboard::~Chessboard() = default;
|
||||
|
||||
void Chessboard::setPrompt(const std::string& prompt) {
|
||||
m_prompt = prompt;
|
||||
setGrammar();
|
||||
}
|
||||
|
||||
void Chessboard::setGrammar() {
|
||||
m_grammar.clear();
|
||||
|
||||
std::string result;
|
||||
if (m_prompt.empty()) {
|
||||
result += "move ::= \" \" ((piece | frompos) \" \" \"to \"?)? topos\n";
|
||||
//result += "move ::= \" \" frompos \" \" \"to \"? topos\n";
|
||||
}
|
||||
else {
|
||||
// result += "move ::= prompt \" \" ((piece | frompos) \" \" \"to \"?)? topos\n"
|
||||
result += "move ::= prompt \" \" frompos \" \" \"to \"? topos\n"
|
||||
"prompt ::= \" " + m_prompt + "\"\n";
|
||||
}
|
||||
|
||||
std::set<Piece::Types> pieceTypes;
|
||||
std::set<char> from_pos;
|
||||
std::set<char> to_pos;
|
||||
auto& pieces = m_moveCounter % 2 ? m_state->blacks : m_state->whites;
|
||||
std::set<size_t> flags;
|
||||
for (auto& p : pieces) {
|
||||
if (p.allowed().empty()) continue;
|
||||
bool addPiece = false;
|
||||
if (!m_inCheck || p.type() == Piece::King) {
|
||||
to_pos.insert(p.allowed().begin(), p.allowed().end());
|
||||
addPiece = !p.allowed().empty();
|
||||
}
|
||||
else {
|
||||
for (auto move : p.allowed()) {
|
||||
if (m_allowedInCheck.count(move)) {
|
||||
to_pos.insert(move);
|
||||
addPiece = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (addPiece) {
|
||||
pieceTypes.insert(p.type());
|
||||
from_pos.insert(p.pos());
|
||||
}
|
||||
}
|
||||
if (pieceTypes.empty()) return;
|
||||
|
||||
result += "piece ::= (";
|
||||
for (auto& p : pieceTypes) result += " \"" + std::string(pieceNames[p]) + "\" |";
|
||||
result.pop_back();
|
||||
result += ")\n\n";
|
||||
|
||||
result += "frompos ::= (";
|
||||
for (auto& p : from_pos) result += " \"" + std::string(positions[p]) + "\" |";
|
||||
result.pop_back();
|
||||
result += ")\n";
|
||||
|
||||
result += "topos ::= (";
|
||||
for (auto& p : to_pos) result += " \"" + std::string(positions[p]) + "\" |";
|
||||
result.pop_back();
|
||||
result += ")\n";
|
||||
|
||||
m_grammar = std::move(result);
|
||||
}
|
||||
|
||||
std::string Chessboard::stringifyBoard() {
|
||||
std::string result;
|
||||
result.reserve(16 + 2 * 64 + 16);
|
||||
for (char rank = 'a'; rank <= 'h'; ++rank) {
|
||||
result.push_back(rank);
|
||||
result.push_back(' ');
|
||||
}
|
||||
result.back() = '\n';
|
||||
for (int i = 7; i >= 0; --i) {
|
||||
for (int j = 0; j < 8; ++j) {
|
||||
auto p = m_state->board[i * 8 + j];
|
||||
if (p) result.push_back(p->initial());
|
||||
else result.push_back((i + j) % 2 ? '.' : '*');
|
||||
result.push_back(' ');
|
||||
}
|
||||
result.push_back('0' + i + 1);
|
||||
result.push_back('\n');
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string Chessboard::process(const std::string& command) {
|
||||
const auto t_start = std::chrono::high_resolution_clock::now();
|
||||
auto color = Piece::Colors(m_moveCounter % 2);
|
||||
Piece* piece = nullptr;
|
||||
auto pos_to = INVALID_POS;
|
||||
if (!parseCommand(command, piece, pos_to)) return "";
|
||||
|
||||
auto pos_from = piece->pos();
|
||||
|
||||
if (!move(*piece, pos_to)) return "";
|
||||
|
||||
flagUpdates(pos_from, pos_to);
|
||||
|
||||
detectChecks();
|
||||
|
||||
auto& enemyPieces = color ? m_state->whites : m_state->blacks;
|
||||
for (auto& p : enemyPieces) p.reinit(*m_state); // only enemy moves needed next
|
||||
|
||||
std::string result = {positions[pos_from][R], positions[pos_from][F], '-', positions[pos_to][R], positions[pos_to][F]};
|
||||
++m_moveCounter;
|
||||
setGrammar();
|
||||
const auto t_end = std::chrono::high_resolution_clock::now();
|
||||
auto t_ms = std::chrono::duration_cast<std::chrono::milliseconds>(t_end - t_start).count();
|
||||
fprintf(stdout, "%s: Move '%s%s%s', (t = %d ms)\n", __func__, "\033[1m", result.data(), "\033[0m", (int) t_ms);
|
||||
if (m_grammar.empty()) result.push_back('#');
|
||||
return result;
|
||||
}
|
||||
|
||||
bool Chessboard::parseCommand(const std::string& command, Piece*& piece, char& pos_to) {
|
||||
auto color = Piece::Colors(m_moveCounter % 2);
|
||||
fprintf(stdout, "%s: Command to %s: '%s%.*s%s'\n", __func__, (color ? "Black" : "White"), "\033[1m", int(command.size()), command.data(), "\033[0m");
|
||||
|
||||
if (command.empty()) return false;
|
||||
auto tokens = split(command, ' ');
|
||||
auto pos_from = INVALID_POS;
|
||||
auto type = Piece::Types::NUM_PIECES;
|
||||
if (tokens.size() == 1) {
|
||||
type = Piece::Types::Pawn;
|
||||
pos_to = strToPos(tokens.front());
|
||||
}
|
||||
else {
|
||||
pos_from = strToPos(tokens.front());
|
||||
if (pos_from == INVALID_POS) type = Piece::Types(strToType(tokens.front()));
|
||||
pos_to = strToPos(tokens.back());
|
||||
}
|
||||
if (pos_to == INVALID_POS) return false;
|
||||
if (pos_from == INVALID_POS) {
|
||||
if (type == Piece::Types::NUM_PIECES) return false;
|
||||
auto& pieces = color ? m_state->blacks : m_state->whites;
|
||||
for (auto& p : pieces) {
|
||||
if (p.type() == type && p.canReach(pos_to)) {
|
||||
pos_from = p.pos();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pos_from == INVALID_POS) return false;
|
||||
if (m_state->board[pos_from] == nullptr) return false;
|
||||
piece = m_state->board[pos_from];
|
||||
if (piece->color() != color) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Chessboard::flagUpdates(char pos_from, char pos_to) {
|
||||
auto color = Piece::Colors(m_moveCounter % 2);
|
||||
auto& enemyPieces = color ? m_state->whites : m_state->blacks;
|
||||
auto& ownPieces = color ? m_state->blacks : m_state->whites;
|
||||
for (auto& p : enemyPieces) {
|
||||
if (p.movePattern(pos_to) || p.movePattern(pos_from)) {
|
||||
updatePins(p);
|
||||
p.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& p : ownPieces) {
|
||||
if (p.movePattern(pos_to) || p.movePattern(pos_from)) {
|
||||
updatePins(p);
|
||||
p.invalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Chessboard::updatePins(Piece& piece) {
|
||||
if (piece.type() == Piece::Pawn || piece.type() == Piece::Knight || piece.type() == Piece::King) return;
|
||||
auto& enemyPieces = piece.color() ? m_state->whites : m_state->blacks;
|
||||
auto& enemyPins = piece.color() ? m_state->whitePins : m_state->blackPins;
|
||||
auto& king = enemyPieces.k;
|
||||
auto it = std::find_if(enemyPins.begin(), enemyPins.end(), [&] (const Pin& pin) { return pin.pinner == &piece; });
|
||||
if (it != enemyPins.end()) {
|
||||
it->pinned->invalidate();
|
||||
enemyPins.erase(it);
|
||||
}
|
||||
if (piece.movePattern(king.pos())) {
|
||||
auto to = positions[king.pos()];
|
||||
auto from = piece.coord();
|
||||
Direction d = normalize({char(to[R] - from[R]), char(to[F] - from[F])});
|
||||
|
||||
auto reached = traverse(piece.pos(), d, Find(m_state->board));
|
||||
auto foundPiece = m_state->board[reached];
|
||||
if (&king == foundPiece) {
|
||||
// check
|
||||
king.invalidate();
|
||||
}
|
||||
else if (foundPiece && foundPiece->color() != piece.color()) {
|
||||
reached = traverse(reached, d, Find(m_state->board));
|
||||
if (&king == m_state->board[reached]) {
|
||||
enemyPins.push_back({d, &piece, foundPiece});
|
||||
foundPiece->invalidate();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Chessboard::detectChecks() {
|
||||
auto color = Piece::Colors(m_moveCounter % 2);
|
||||
auto& enemyPieces = color ? m_state->whites : m_state->blacks;
|
||||
auto& ownPieces = color ? m_state->blacks : m_state->whites;
|
||||
auto& king = enemyPieces.k;
|
||||
auto& pawnAttackLeft = color ? SW : NW;
|
||||
auto& pawnAttackRight = color ? SE : NE;
|
||||
for (auto& p : ownPieces) {
|
||||
if (!p.movePattern(king.pos())) continue;
|
||||
auto to = positions[king.pos()];
|
||||
auto from = p.coord();
|
||||
|
||||
if (p.type() == Piece::Knight) {
|
||||
if (!m_inCheck) {
|
||||
m_allowedInCheck = { p.pos() };
|
||||
}
|
||||
else {
|
||||
m_allowedInCheck.clear();
|
||||
}
|
||||
m_inCheck = true;
|
||||
}
|
||||
else if (p.type() == Piece::Pawn) {
|
||||
Direction d {char(to[R] - from[R]), char(to[F] - from[F])};
|
||||
if (d == pawnAttackLeft || d == pawnAttackRight) {
|
||||
if (!m_inCheck) {
|
||||
m_allowedInCheck = { p.pos() };
|
||||
}
|
||||
else {
|
||||
m_allowedInCheck.clear();
|
||||
}
|
||||
m_inCheck = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
Direction d = normalize({char(to[R] - from[R]), char(to[F] - from[F])});
|
||||
std::set<char> tmp;
|
||||
auto pos = traverse(p.pos(), d, Add(m_state->board, tmp, king.color()));
|
||||
if (pos == king.pos()) {
|
||||
tmp.insert(p.pos());
|
||||
if (!m_inCheck) {
|
||||
m_allowedInCheck = std::move(tmp);
|
||||
}
|
||||
else {
|
||||
m_allowedInCheck.clear();
|
||||
}
|
||||
m_inCheck = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool Chessboard::move(Piece& piece, char pos_to) {
|
||||
auto& allowed = piece.allowed();
|
||||
|
||||
if (allowed.count(pos_to) == 0 || (m_inCheck && piece.type() != Piece::King && m_allowedInCheck.count(pos_to) == 0)) return false;
|
||||
if (m_state->board[pos_to] && m_state->board[pos_to]->color() == piece.color()) return false;
|
||||
if (m_state->board[pos_to]) m_state->board[pos_to]->take();
|
||||
m_state->board[piece.pos()] = nullptr;
|
||||
m_state->board[pos_to] = &piece;
|
||||
piece.setPos(pos_to);
|
||||
|
||||
m_inCheck = false;
|
||||
m_allowedInCheck.clear();
|
||||
|
||||
return true;
|
||||
}
|
33
examples/wchess/libwchess/Chessboard.h
Normal file
@ -0,0 +1,33 @@
|
||||
#pragma once
|
||||
#include <string>
|
||||
#include <set>
|
||||
#include <memory>
|
||||
|
||||
// just basic validation
|
||||
// fixme: missing en passant, castling, promotion, etc.
|
||||
struct State;
|
||||
class Piece;
|
||||
class Chessboard {
|
||||
public:
|
||||
Chessboard();
|
||||
~Chessboard();
|
||||
std::string process(const std::string& command);
|
||||
std::string stringifyBoard();
|
||||
const std::string& grammar() { return m_grammar; }
|
||||
const std::string& prompt() { return m_prompt; }
|
||||
void setPrompt(const std::string& prompt);
|
||||
private:
|
||||
bool parseCommand(const std::string& command, Piece*& piece, char& pos_to);
|
||||
bool move(Piece& piece, char pos);
|
||||
void flagUpdates(char pos_from, char pos_to);
|
||||
void updatePins(Piece& piece);
|
||||
void detectChecks();
|
||||
void setGrammar();
|
||||
|
||||
std::unique_ptr<State> m_state;
|
||||
std::set<char> m_allowedInCheck;
|
||||
bool m_inCheck = false;
|
||||
int m_moveCounter = 0;
|
||||
std::string m_grammar;
|
||||
std::string m_prompt;
|
||||
};
|
193
examples/wchess/libwchess/WChess.cpp
Normal file
@ -0,0 +1,193 @@
|
||||
#include "WChess.h"
|
||||
#include "Chessboard.h"
|
||||
#include "grammar-parser.h"
|
||||
#include "common.h"
|
||||
#include <thread>
|
||||
|
||||
WChess::WChess(whisper_context * ctx,
|
||||
const whisper_full_params & wparams,
|
||||
callbacks cb,
|
||||
settings s)
|
||||
: m_ctx(ctx)
|
||||
, m_wparams(wparams)
|
||||
, m_cb(cb)
|
||||
, m_settings(s)
|
||||
, m_board(new Chessboard())
|
||||
{}
|
||||
|
||||
WChess::~WChess() = default;
|
||||
|
||||
void WChess::set_move(const std::string& moves, float prob) const {
|
||||
if (m_cb.set_move) (*m_cb.set_move)(moves, prob);
|
||||
}
|
||||
|
||||
void WChess::set_grammar(const std::string& grammar) const {
|
||||
if (m_cb.set_grammar) (*m_cb.set_grammar)(grammar);
|
||||
}
|
||||
|
||||
bool WChess::get_audio(std::vector<float>& pcmf32) const {
|
||||
if (m_cb.get_audio) return (*m_cb.get_audio)(pcmf32);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string WChess::stringify_board() const {
|
||||
return m_board->stringifyBoard();
|
||||
}
|
||||
|
||||
std::string WChess::get_grammar() const {
|
||||
return m_board->grammar();
|
||||
}
|
||||
|
||||
void WChess::run() {
|
||||
bool have_prompt = true;
|
||||
bool ask_prompt = !have_prompt;
|
||||
|
||||
float logprob_min = 0.0f;
|
||||
|
||||
float logprob_sum = 0.0f;
|
||||
|
||||
int n_tokens = 0;
|
||||
|
||||
std::vector<float> pcmf32_cur;
|
||||
std::vector<float> pcmf32_prompt;
|
||||
|
||||
const std::string k_prompt = have_prompt ? "" : "rook to d4, f3";
|
||||
int64_t t_ms = 0;
|
||||
|
||||
if (ask_prompt) {
|
||||
fprintf(stdout, "\n");
|
||||
fprintf(stdout, "%s: Say the following phrase: '%s%s%s'\n", __func__, "\033[1m", k_prompt.c_str(), "\033[0m");
|
||||
fprintf(stdout, "\n");
|
||||
|
||||
ask_prompt = false;
|
||||
}
|
||||
|
||||
while (get_audio(pcmf32_cur)) {
|
||||
if (!pcmf32_cur.empty()) {
|
||||
// fprintf(stdout, "%s: Processing ...\n", __func__);
|
||||
|
||||
if (!have_prompt) {
|
||||
const auto txt = ::trim(transcribe(pcmf32_cur, logprob_min, logprob_sum, n_tokens, t_ms));
|
||||
|
||||
fprintf(stdout, "%s: Heard '%s%s%s', (t = %d ms)\n", __func__, "\033[1m", txt.c_str(), "\033[0m", (int) t_ms);
|
||||
|
||||
const float sim = similarity(txt, k_prompt);
|
||||
|
||||
if (txt.length() < 0.8*k_prompt.length() || txt.length() > 1.2*k_prompt.length() || sim < 0.8f) {
|
||||
fprintf(stdout, "%s: WARNING: prompt not recognized, try again\n", __func__);
|
||||
ask_prompt = true;
|
||||
} else {
|
||||
fprintf(stdout, "\n");
|
||||
fprintf(stdout, "%s: The prompt has been recognized!\n", __func__);
|
||||
fprintf(stdout, "%s: Waiting for voice commands ...\n", __func__);
|
||||
fprintf(stdout, "\n");
|
||||
|
||||
// save the audio for the prompt
|
||||
pcmf32_prompt = pcmf32_cur;
|
||||
have_prompt = true;
|
||||
m_board->setPrompt(k_prompt);
|
||||
}
|
||||
} else {
|
||||
if (!pcmf32_prompt.empty()) pcmf32_cur.insert(pcmf32_cur.begin(), pcmf32_prompt.begin(), pcmf32_prompt.end());
|
||||
constexpr size_t MIN_SIZE = 1.2 * WHISPER_SAMPLE_RATE;
|
||||
if (MIN_SIZE > pcmf32_cur.size()) pcmf32_cur.insert(pcmf32_cur.begin(), MIN_SIZE - pcmf32_cur.size(), 0.0f);
|
||||
|
||||
// fprintf(stdout, "%s: grammar rules:\n'%s'\n", __func__, m_board->grammar().c_str());
|
||||
|
||||
auto grammar_parsed = grammar_parser::parse(m_board->grammar().c_str());
|
||||
auto grammar_rules = grammar_parsed.c_rules();
|
||||
|
||||
m_wparams.grammar_rules = grammar_rules.data();
|
||||
m_wparams.n_grammar_rules = grammar_rules.size();
|
||||
|
||||
m_wparams.i_start_rule = grammar_parsed.symbol_ids.at("move");
|
||||
auto txt = ::trim(transcribe(pcmf32_cur, logprob_min, logprob_sum, n_tokens, t_ms));
|
||||
|
||||
const float p = 100.0f * std::exp(logprob_min);
|
||||
|
||||
fprintf(stdout, "%s: heard '%s'\n", __func__, txt.c_str());
|
||||
|
||||
// find the prompt in the text
|
||||
float best_sim = 0.0f;
|
||||
size_t best_len = 0;
|
||||
for (int n = 0.8*k_prompt.size(); n <= 1.2*k_prompt.size(); ++n) {
|
||||
const auto prompt = txt.substr(0, n);
|
||||
|
||||
const float sim = similarity(prompt, k_prompt);
|
||||
|
||||
//fprintf(stderr, "%s: prompt = '%s', sim = %f\n", __func__, prompt.c_str(), sim);
|
||||
|
||||
if (sim > best_sim) {
|
||||
best_sim = sim;
|
||||
best_len = n;
|
||||
}
|
||||
}
|
||||
|
||||
fprintf(stdout, "%s: DEBUG: txt = '%s', prob = %.2f%%\n", __func__, txt.c_str(), p);
|
||||
std::string command = ::trim(txt.substr(best_len));
|
||||
|
||||
fprintf(stdout, "%s: Command '%s%s%s', (t = %d ms)\n", __func__, "\033[1m", command.c_str(), "\033[0m", (int) t_ms);
|
||||
fprintf(stdout, "\n");
|
||||
|
||||
if (!command.empty()) {
|
||||
set_move(m_board->process(command), p);
|
||||
set_grammar(m_board->grammar());
|
||||
}
|
||||
if (m_board->grammar().empty()) {
|
||||
fprintf(stdout, "%s: No more moves possible\n", __func__);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ask_prompt) {
|
||||
fprintf(stdout, "\n");
|
||||
fprintf(stdout, "%s: Say the following phrase: '%s%s%s'\n", __func__, "\033[1m", k_prompt.c_str(), "\033[0m");
|
||||
fprintf(stdout, "\n");
|
||||
|
||||
ask_prompt = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::string WChess::transcribe(
|
||||
const std::vector<float> & pcmf32,
|
||||
float & logprob_min,
|
||||
float & logprob_sum,
|
||||
int & n_tokens,
|
||||
int64_t & t_ms) {
|
||||
const auto t_start = std::chrono::high_resolution_clock::now();
|
||||
|
||||
logprob_min = 0.0f;
|
||||
logprob_sum = 0.0f;
|
||||
n_tokens = 0;
|
||||
t_ms = 0;
|
||||
|
||||
if (whisper_full(m_ctx, m_wparams, pcmf32.data(), pcmf32.size()) != 0) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::string result;
|
||||
|
||||
const int n_segments = whisper_full_n_segments(m_ctx);
|
||||
for (int i = 0; i < n_segments; ++i) {
|
||||
const char * text = whisper_full_get_segment_text(m_ctx, i);
|
||||
|
||||
result += text;
|
||||
|
||||
const int n = whisper_full_n_tokens(m_ctx, i);
|
||||
for (int j = 0; j < n; ++j) {
|
||||
const auto token = whisper_full_get_token_data(m_ctx, i, j);
|
||||
|
||||
if(token.plog > 0.0f) return {};
|
||||
logprob_min = std::min(logprob_min, token.plog);
|
||||
logprob_sum += token.plog;
|
||||
++n_tokens;
|
||||
}
|
||||
}
|
||||
|
||||
const auto t_end = std::chrono::high_resolution_clock::now();
|
||||
t_ms = std::chrono::duration_cast<std::chrono::milliseconds>(t_end - t_start).count();
|
||||
|
||||
return result;
|
||||
}
|
63
examples/wchess/libwchess/WChess.h
Normal file
@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
#include "whisper.h"
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
|
||||
class Chessboard;
|
||||
|
||||
class WChess {
|
||||
public:
|
||||
using CheckRunningCb = bool (*)();
|
||||
using GetAudioCb = bool (*)(std::vector<float> &);
|
||||
using SetMovesCb = void (*)(const std::string &, float);
|
||||
using SetGrammarCb = void (*)(const std::string &);
|
||||
using ClearAudioCb = void (*)();
|
||||
|
||||
struct callbacks {
|
||||
GetAudioCb get_audio = nullptr;
|
||||
SetMovesCb set_move = nullptr;
|
||||
SetGrammarCb set_grammar = nullptr;
|
||||
};
|
||||
|
||||
struct settings {
|
||||
int32_t vad_ms = 2000;
|
||||
int32_t prompt_ms = 5000;
|
||||
int32_t command_ms = 4000;
|
||||
float vad_thold = 0.2f;
|
||||
float freq_thold = 100.0f;
|
||||
bool print_energy = false;
|
||||
};
|
||||
|
||||
WChess(
|
||||
whisper_context * ctx,
|
||||
const whisper_full_params & wparams,
|
||||
callbacks cb,
|
||||
settings s
|
||||
);
|
||||
~WChess();
|
||||
|
||||
void run();
|
||||
|
||||
std::string stringify_board() const;
|
||||
|
||||
std::string get_grammar() const;
|
||||
|
||||
private:
|
||||
bool get_audio(std::vector<float>& pcmf32) const;
|
||||
void set_move(const std::string& moves, float prob) const;
|
||||
void set_grammar(const std::string& grammar) const;
|
||||
|
||||
std::string transcribe(
|
||||
const std::vector<float> & pcmf32,
|
||||
float & logprob_min,
|
||||
float & logprob_sum,
|
||||
int & n_tokens,
|
||||
int64_t & t_ms);
|
||||
|
||||
whisper_context * m_ctx;
|
||||
whisper_full_params m_wparams;
|
||||
const callbacks m_cb;
|
||||
const settings m_settings;
|
||||
std::unique_ptr<Chessboard> m_board;
|
||||
};
|
117
examples/wchess/libwchess/test-chessboard.cpp
Normal file
@ -0,0 +1,117 @@
|
||||
#include "Chessboard.h"
|
||||
|
||||
#define ASSERT(x) \
|
||||
do { \
|
||||
if (!(x)) { \
|
||||
fprintf(stderr, "ASSERT: %s:%d: %s\n", __FILE__, __LINE__, #x); \
|
||||
fflush(stderr); \
|
||||
exit(1); \
|
||||
} \
|
||||
} while (0)
|
||||
|
||||
|
||||
int main() {
|
||||
{
|
||||
Chessboard chess;
|
||||
|
||||
ASSERT(chess.process("pawn to d4") == "d2-d4");
|
||||
ASSERT(chess.process("e5") == "e7-e5");
|
||||
ASSERT(chess.process("c1 h6") == "c1-h6");
|
||||
ASSERT(chess.process("queen h4") == "d8-h4");
|
||||
ASSERT(chess.process("bishop to g5") == "h6-g5");
|
||||
ASSERT(chess.process("bishop to b4") == "f8-b4");
|
||||
ASSERT(chess.process("c4") == "");
|
||||
ASSERT(chess.process("knight c3") == "b1-c3");
|
||||
ASSERT(chess.process("knight c6") == "b8-c6");
|
||||
ASSERT(chess.process("f3") == "");
|
||||
}
|
||||
|
||||
{
|
||||
Chessboard chess;
|
||||
|
||||
ASSERT(chess.process("d4") == "d2-d4");
|
||||
ASSERT(chess.process("e5") == "e7-e5");
|
||||
ASSERT(chess.process("e4") == "e2-e4");
|
||||
ASSERT(chess.process("queen h4") == "d8-h4");
|
||||
ASSERT(chess.process("queen h5") == "d1-h5");
|
||||
ASSERT(chess.process("f5") == "");
|
||||
ASSERT(chess.process("g6") == "g7-g6");
|
||||
ASSERT(chess.process("knight e2") == "g1-e2");
|
||||
ASSERT(chess.process("f5") == "f7-f5");
|
||||
ASSERT(chess.process("knight g3") == "e2-g3");
|
||||
ASSERT(chess.process("g5") == "");
|
||||
ASSERT(chess.process("king e7") == "e8-e7");
|
||||
ASSERT(chess.process("f4") == "f2-f4");
|
||||
ASSERT(chess.process("g5") == "g6-g5");
|
||||
}
|
||||
|
||||
{
|
||||
Chessboard chess;
|
||||
|
||||
ASSERT(chess.process("e4") == "e2-e4");
|
||||
ASSERT(chess.process("c5") == "c7-c5");
|
||||
ASSERT(chess.process("e5") == "e4-e5");
|
||||
ASSERT(chess.process("c4") == "c5-c4");
|
||||
ASSERT(chess.process("e6") == "e5-e6");
|
||||
ASSERT(chess.process("c3") == "c4-c3");
|
||||
ASSERT(chess.process("e7") == "");
|
||||
ASSERT(chess.process("f7") == "e6-f7");
|
||||
ASSERT(chess.process("d2") == "");
|
||||
ASSERT(chess.process("king to f7") == "e8-f7");
|
||||
ASSERT(chess.process("f4") == "f2-f4");
|
||||
ASSERT(chess.process("d2") == "c3-d2");
|
||||
ASSERT(chess.process("f5") == "");
|
||||
ASSERT(chess.process("king to e2") == "e1-e2");
|
||||
ASSERT(chess.process("king to g6") == "f7-g6");
|
||||
ASSERT(chess.process("f5") == "f4-f5");
|
||||
ASSERT(chess.process("e6") == "");
|
||||
ASSERT(chess.process("king to h5") == "g6-h5");
|
||||
ASSERT(chess.process("g4") == "g2-g4");
|
||||
ASSERT(chess.process("king to g5") == "h5-g5");
|
||||
ASSERT(chess.process("h4") == "h2-h4");
|
||||
ASSERT(chess.process("king to h5") == "");
|
||||
ASSERT(chess.process("king to g6") == "");
|
||||
ASSERT(chess.process("king to h6") == "g5-h6");
|
||||
ASSERT(chess.process("bishop to d2") == "c1-d2");
|
||||
ASSERT(chess.process("king to g5") == "");
|
||||
ASSERT(chess.process("g5") == "g7-g5");
|
||||
}
|
||||
|
||||
{
|
||||
Chessboard chess;
|
||||
ASSERT(chess.process("f4") == "f2-f4");
|
||||
ASSERT(chess.process("e5") == "e7-e5");
|
||||
ASSERT(chess.process("g4") == "g2-g4");
|
||||
ASSERT(chess.process("queen to h4") == "d8-h4#");
|
||||
ASSERT(chess.process("knight f3") == "");
|
||||
ASSERT(chess.grammar().empty());
|
||||
}
|
||||
|
||||
{
|
||||
Chessboard chess;
|
||||
ASSERT(chess.process("f4") == "f2-f4");
|
||||
ASSERT(chess.process("e5") == "e7-e5");
|
||||
ASSERT(chess.process("g4") == "g2-g4");
|
||||
ASSERT(chess.process("d5") == "d7-d5");
|
||||
ASSERT(chess.process("g1 f3") == "g1-f3");
|
||||
ASSERT(chess.process("queen to h4") == "d8-h4");
|
||||
ASSERT(!chess.grammar().empty());
|
||||
}
|
||||
|
||||
{
|
||||
Chessboard chess;
|
||||
ASSERT(chess.process("knight c3") == "b1-c3");
|
||||
ASSERT(chess.process("knight c6") == "b8-c6");
|
||||
ASSERT(chess.process("knight b5") == "c3-b5");
|
||||
ASSERT(chess.process("knight f6") == "g8-f6");
|
||||
ASSERT(chess.process("knight d6") == "b5-d6");
|
||||
ASSERT(chess.process("knight d4") == "");
|
||||
ASSERT(chess.process("d6") == "c7-d6");
|
||||
ASSERT(chess.process("e4") == "e2-e4");
|
||||
ASSERT(chess.process("knight d4") == "c6-d4");
|
||||
ASSERT(chess.process("d3") == "d2-d3");
|
||||
ASSERT(chess.process("knight e4") == "f6-e4");
|
||||
ASSERT(chess.process("king to e2") == "");
|
||||
ASSERT(chess.process("king to d2") == "");
|
||||
}
|
||||
}
|
8
examples/wchess/wchess.cmd/CMakeLists.txt
Normal file
@ -0,0 +1,8 @@
|
||||
if (WHISPER_SDL2)
|
||||
set(TARGET wchess)
|
||||
add_executable(${TARGET} wchess.cmd.cpp)
|
||||
|
||||
include(DefaultTargetOptions)
|
||||
|
||||
target_link_libraries(${TARGET} PRIVATE wchess-core common-sdl ${CMAKE_THREAD_LIBS_INIT})
|
||||
endif ()
|
247
examples/wchess/wchess.cmd/wchess.cmd.cpp
Normal file
@ -0,0 +1,247 @@
|
||||
// Command line voice assisted chess
|
||||
//
|
||||
// Speak chess move commands to the microphone.
|
||||
// The moves will translated to chessboard positions.
|
||||
//
|
||||
//
|
||||
|
||||
#include "WChess.h"
|
||||
#include "common-sdl.h"
|
||||
#include <iostream>
|
||||
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
|
||||
// command-line parameters
|
||||
struct whisper_params {
|
||||
int32_t n_threads = std::min(4, (int32_t) std::thread::hardware_concurrency());
|
||||
int32_t prompt_ms = 5000;
|
||||
int32_t command_ms = 8000;
|
||||
int32_t capture_id = -1;
|
||||
int32_t max_tokens = 32;
|
||||
int32_t audio_ctx = 0;
|
||||
|
||||
float vad_thold = 0.6f;
|
||||
float freq_thold = 100.0f;
|
||||
|
||||
float grammar_penalty = 100.0f;
|
||||
|
||||
bool speed_up = false;
|
||||
bool translate = false;
|
||||
bool print_special = false;
|
||||
bool print_energy = false;
|
||||
bool no_timestamps = true;
|
||||
bool use_gpu = true;
|
||||
|
||||
std::string language = "en";
|
||||
std::string model = "models/ggml-base.en.bin";
|
||||
std::string fname_out;
|
||||
std::string commands;
|
||||
std::string prompt;
|
||||
std::string context;
|
||||
std::string grammar;
|
||||
};
|
||||
|
||||
void whisper_print_usage(int /*argc*/, char ** argv, const whisper_params & params) {
|
||||
fprintf(stderr, "\n");
|
||||
fprintf(stderr, "usage: %s [options]\n", argv[0]);
|
||||
fprintf(stderr, "\n");
|
||||
fprintf(stderr, "options:\n");
|
||||
fprintf(stderr, " -h, --help [default] show this help message and exit\n");
|
||||
fprintf(stderr, " -t N, --threads N [%-7d] number of threads to use during computation\n", params.n_threads);
|
||||
fprintf(stderr, " -pms N, --prompt-ms N [%-7d] prompt duration in milliseconds\n", params.prompt_ms);
|
||||
fprintf(stderr, " -cms N, --command-ms N [%-7d] command duration in milliseconds\n", params.command_ms);
|
||||
fprintf(stderr, " -c ID, --capture ID [%-7d] capture device ID\n", params.capture_id);
|
||||
fprintf(stderr, " -mt N, --max-tokens N [%-7d] maximum number of tokens per audio chunk\n", params.max_tokens);
|
||||
fprintf(stderr, " -ac N, --audio-ctx N [%-7d] audio context size (0 - all)\n", params.audio_ctx);
|
||||
fprintf(stderr, " -vth N, --vad-thold N [%-7.2f] voice activity detection threshold\n", params.vad_thold);
|
||||
fprintf(stderr, " -fth N, --freq-thold N [%-7.2f] high-pass frequency cutoff\n", params.freq_thold);
|
||||
fprintf(stderr, " -su, --speed-up [%-7s] speed up audio by x2 (reduced accuracy)\n", params.speed_up ? "true" : "false");
|
||||
fprintf(stderr, " -tr, --translate [%-7s] translate from source language to english\n", params.translate ? "true" : "false");
|
||||
fprintf(stderr, " -ps, --print-special [%-7s] print special tokens\n", params.print_special ? "true" : "false");
|
||||
fprintf(stderr, " -pe, --print-energy [%-7s] print sound energy (for debugging)\n", params.print_energy ? "true" : "false");
|
||||
fprintf(stderr, " -ng, --no-gpu [%-7s] disable GPU\n", params.use_gpu ? "false" : "true");
|
||||
fprintf(stderr, " -l LANG, --language LANG [%-7s] spoken language\n", params.language.c_str());
|
||||
fprintf(stderr, " -m FNAME, --model FNAME [%-7s] model path\n", params.model.c_str());
|
||||
fprintf(stderr, " -f FNAME, --file FNAME [%-7s] text output file name\n", params.fname_out.c_str());
|
||||
fprintf(stderr, " -cmd FNAME, --commands FNAME [%-7s] text file with allowed commands\n", params.commands.c_str());
|
||||
fprintf(stderr, " -p, --prompt [%-7s] the required activation prompt\n", params.prompt.c_str());
|
||||
fprintf(stderr, " -ctx, --context [%-7s] sample text to help the transcription\n", params.context.c_str());
|
||||
fprintf(stderr, " --grammar-penalty N [%-7.1f] scales down logits of nongrammar tokens\n", params.grammar_penalty);
|
||||
fprintf(stderr, "\n");
|
||||
}
|
||||
|
||||
bool whisper_params_parse(int argc, char ** argv, whisper_params & params) {
|
||||
for (int i = 1; i < argc; i++) {
|
||||
std::string arg = argv[i];
|
||||
|
||||
if (arg == "-h" || arg == "--help") {
|
||||
whisper_print_usage(argc, argv, params);
|
||||
exit(0);
|
||||
}
|
||||
else if (arg == "-t" || arg == "--threads") { params.n_threads = std::stoi(argv[++i]); }
|
||||
else if (arg == "-pms" || arg == "--prompt-ms") { params.prompt_ms = std::stoi(argv[++i]); }
|
||||
else if (arg == "-cms" || arg == "--command-ms") { params.command_ms = std::stoi(argv[++i]); }
|
||||
else if (arg == "-c" || arg == "--capture") { params.capture_id = std::stoi(argv[++i]); }
|
||||
else if (arg == "-mt" || arg == "--max-tokens") { params.max_tokens = std::stoi(argv[++i]); }
|
||||
else if (arg == "-ac" || arg == "--audio-ctx") { params.audio_ctx = std::stoi(argv[++i]); }
|
||||
else if (arg == "-vth" || arg == "--vad-thold") { params.vad_thold = std::stof(argv[++i]); }
|
||||
else if (arg == "-fth" || arg == "--freq-thold") { params.freq_thold = std::stof(argv[++i]); }
|
||||
else if (arg == "-su" || arg == "--speed-up") { params.speed_up = true; }
|
||||
else if (arg == "-tr" || arg == "--translate") { params.translate = true; }
|
||||
else if (arg == "-ps" || arg == "--print-special") { params.print_special = true; }
|
||||
else if (arg == "-pe" || arg == "--print-energy") { params.print_energy = true; }
|
||||
else if (arg == "-ng" || arg == "--no-gpu") { params.use_gpu = false; }
|
||||
else if (arg == "-l" || arg == "--language") { params.language = argv[++i]; }
|
||||
else if (arg == "-m" || arg == "--model") { params.model = argv[++i]; }
|
||||
else if (arg == "-f" || arg == "--file") { params.fname_out = argv[++i]; }
|
||||
else if (arg == "-cmd" || arg == "--commands") { params.commands = argv[++i]; }
|
||||
else if (arg == "-p" || arg == "--prompt") { params.prompt = argv[++i]; }
|
||||
else if (arg == "-ctx" || arg == "--context") { params.context = argv[++i]; }
|
||||
else if ( arg == "--grammar-penalty") { params.grammar_penalty = std::stof(argv[++i]); }
|
||||
else {
|
||||
fprintf(stderr, "error: unknown argument: %s\n", arg.c_str());
|
||||
whisper_print_usage(argc, argv, params);
|
||||
exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unique_ptr<WChess> g_wchess;
|
||||
int g_moveCount = 0;
|
||||
void set_move(const std::string & move, float) {
|
||||
if (!move.empty()) {
|
||||
g_moveCount++;
|
||||
fprintf(stdout, "Move: %s\n\n", move.c_str());
|
||||
}
|
||||
else fprintf(stdout, "Move rejected\n\n");
|
||||
fprintf(stdout, "%s\n", g_wchess->stringify_board().c_str());
|
||||
fprintf(stdout, "%s\n", g_moveCount ? "White's turn" : "Black's turn");
|
||||
}
|
||||
|
||||
audio_async g_audio(30*1000);
|
||||
bool g_listening = false;
|
||||
std::vector<float> g_pcmf32;
|
||||
|
||||
bool read_input() {
|
||||
std::string input;
|
||||
while (true) {
|
||||
fprintf(stdout, "[(l)isten/(p)ause/(q)uit]: ");
|
||||
std::cin >> input;
|
||||
fprintf(stdout, "\n");
|
||||
if (input[0] == 'q') {
|
||||
fprintf(stdout, "Quitting\n");
|
||||
return false;
|
||||
}
|
||||
if (input[0] == 'l') {
|
||||
if (!g_listening) {
|
||||
fprintf(stdout, "Listening\n");
|
||||
g_listening = true;
|
||||
g_pcmf32.clear();
|
||||
g_audio.resume();
|
||||
g_audio.clear();
|
||||
}
|
||||
else fprintf(stdout, "Still listening\n");
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
if (g_listening) {
|
||||
g_listening = false;
|
||||
g_audio.get(0, g_pcmf32);
|
||||
g_audio.pause();
|
||||
fprintf(stdout, "Processing\n");
|
||||
}
|
||||
else fprintf(stdout, "Not listening\n");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool get_audio(std::vector<float> & pcmf32_cur) {
|
||||
if (!read_input()) return false;
|
||||
if (!g_pcmf32.empty()) pcmf32_cur = std::move(g_pcmf32);
|
||||
else pcmf32_cur.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
int main(int argc, char ** argv) {
|
||||
whisper_params params;
|
||||
|
||||
if (whisper_params_parse(argc, argv, params) == false) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (whisper_lang_id(params.language.c_str()) == -1) {
|
||||
fprintf(stderr, "error: unknown language '%s'\n", params.language.c_str());
|
||||
whisper_print_usage(argc, argv, params);
|
||||
exit(0);
|
||||
}
|
||||
|
||||
// whisper init
|
||||
|
||||
struct whisper_context_params cparams;
|
||||
cparams.use_gpu = params.use_gpu;
|
||||
|
||||
struct whisper_context * ctx = whisper_init_from_file_with_params(params.model.c_str(), cparams);
|
||||
if (!ctx) {
|
||||
fprintf(stderr, "%s: whisper_init_from_file_with_params() failed!\n", __func__);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// init audio
|
||||
|
||||
if (!g_audio.init(params.capture_id, WHISPER_SAMPLE_RATE)) {
|
||||
fprintf(stderr, "%s: audio.init() failed!\n", __func__);
|
||||
return 1;
|
||||
}
|
||||
|
||||
struct whisper_full_params wparams = whisper_full_default_params(whisper_sampling_strategy::WHISPER_SAMPLING_GREEDY);
|
||||
wparams.offset_ms = 0;
|
||||
wparams.translate = false;
|
||||
wparams.no_context = true;
|
||||
wparams.single_segment = true;
|
||||
wparams.print_realtime = false;
|
||||
wparams.print_progress = false;
|
||||
wparams.print_timestamps = true;
|
||||
wparams.print_special = false;
|
||||
wparams.no_timestamps = true;
|
||||
|
||||
wparams.max_tokens = 32;
|
||||
wparams.audio_ctx = 768; // partial encoder context for better performance
|
||||
|
||||
wparams.temperature = 0.0f;
|
||||
wparams.temperature_inc = 2.0f;
|
||||
wparams.greedy.best_of = 1;
|
||||
|
||||
wparams.beam_search.beam_size = 1;
|
||||
|
||||
wparams.language = "en";
|
||||
|
||||
wparams.grammar_penalty = 100.0;
|
||||
|
||||
wparams.initial_prompt = params.context.data();
|
||||
|
||||
WChess::callbacks cb;
|
||||
cb.get_audio = get_audio;
|
||||
cb.set_move = set_move;
|
||||
|
||||
WChess::settings s;
|
||||
s.vad_ms = 2000;
|
||||
s.prompt_ms = params.prompt_ms;
|
||||
s.command_ms = params.command_ms;
|
||||
s.vad_thold = params.vad_thold;
|
||||
s.freq_thold = params.freq_thold;
|
||||
s.print_energy = params.print_energy;
|
||||
|
||||
g_wchess.reset(new WChess(ctx, wparams, cb, s));
|
||||
set_move("start", 0);
|
||||
g_wchess->run();
|
||||
|
||||
whisper_print_timings(ctx);
|
||||
whisper_free(ctx);
|
||||
|
||||
return 0;
|
||||
}
|
51
examples/wchess/wchess.wasm/CMakeLists.txt
Normal file
@ -0,0 +1,51 @@
|
||||
set(TARGET wchess.wasm)
|
||||
|
||||
add_executable(${TARGET}
|
||||
wchess.wasm.cpp
|
||||
)
|
||||
|
||||
include(DefaultTargetOptions)
|
||||
|
||||
target_link_libraries(${TARGET} PRIVATE
|
||||
common
|
||||
wchess-core
|
||||
)
|
||||
|
||||
unset(EXTRA_FLAGS)
|
||||
|
||||
if (WHISPER_WASM_SINGLE_FILE)
|
||||
set(EXTRA_FLAGS "-s SINGLE_FILE=1")
|
||||
message(STATUS "Embedding WASM inside chess.js")
|
||||
|
||||
add_custom_command(
|
||||
TARGET ${TARGET} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_BINARY_DIR}/bin/${TARGET}.js
|
||||
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${TARGET}/js/chess.js
|
||||
)
|
||||
endif()
|
||||
|
||||
set_target_properties(${TARGET} PROPERTIES LINK_FLAGS " \
|
||||
--bind \
|
||||
-s USE_PTHREADS=1 \
|
||||
-s PTHREAD_POOL_SIZE=8 \
|
||||
-s INITIAL_MEMORY=1024MB \
|
||||
-s TOTAL_MEMORY=1024MB \
|
||||
-s FORCE_FILESYSTEM=1 \
|
||||
-s EXPORTED_RUNTIME_METHODS=\"['print', 'printErr', 'ccall', 'cwrap']\" \
|
||||
${EXTRA_FLAGS} \
|
||||
")
|
||||
|
||||
|
||||
add_custom_command(
|
||||
TARGET ${TARGET} POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/chessboardjs-1.0.0
|
||||
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${TARGET}/
|
||||
COMMAND ${CMAKE_COMMAND} -E copy
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/jquery-3.7.1.min.js
|
||||
${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${TARGET}/js/
|
||||
)
|
||||
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/index-tmpl.html ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${TARGET}/index.html @ONLY)
|
||||
configure_file(${CMAKE_SOURCE_DIR}/examples/helpers.js ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${TARGET}/js/helpers.js @ONLY)
|
@ -0,0 +1,54 @@
|
||||
/*! chessboard.js v1.0.0 | (c) 2019 Chris Oakman | MIT License chessboardjs.com/license */
|
||||
|
||||
.clearfix-7da63 {
|
||||
clear: both;
|
||||
}
|
||||
|
||||
.board-b72b1 {
|
||||
border: 2px solid #404040;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
|
||||
.square-55d63 {
|
||||
float: left;
|
||||
position: relative;
|
||||
|
||||
/* disable any native browser highlighting */
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-select: none;
|
||||
-khtml-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.white-1e1d7 {
|
||||
background-color: #f0d9b5;
|
||||
color: #b58863;
|
||||
}
|
||||
|
||||
.black-3c85d {
|
||||
background-color: #b58863;
|
||||
color: #f0d9b5;
|
||||
}
|
||||
|
||||
.highlight1-32417, .highlight2-9c5d2 {
|
||||
box-shadow: inset 0 0 3px 3px yellow;
|
||||
}
|
||||
|
||||
.notation-322f9 {
|
||||
cursor: default;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
font-size: 14px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.alpha-d2270 {
|
||||
bottom: 1px;
|
||||
right: 3px;
|
||||
}
|
||||
|
||||
.numeric-fc462 {
|
||||
top: 2px;
|
||||
left: 2px;
|
||||
}
|
2
examples/wchess/wchess.wasm/chessboardjs-1.0.0/css/chessboard-1.0.0.min.css
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
/*! chessboard.js v1.0.0 | (c) 2019 Chris Oakman | MIT License chessboardjs.com/license */
|
||||
.clearfix-7da63{clear:both}.board-b72b1{border:2px solid #404040;box-sizing:content-box}.square-55d63{float:left;position:relative;-webkit-touch-callout:none;-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.white-1e1d7{background-color:#f0d9b5;color:#b58863}.black-3c85d{background-color:#b58863;color:#f0d9b5}.highlight1-32417,.highlight2-9c5d2{box-shadow:inset 0 0 3px 3px #ff0}.notation-322f9{cursor:default;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:14px;position:absolute}.alpha-d2270{bottom:1px;right:3px}.numeric-fc462{top:2px;left:2px}
|
After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 2.9 KiB |
After Width: | Height: | Size: 1.8 KiB |
After Width: | Height: | Size: 777 B |
After Width: | Height: | Size: 2.6 KiB |
After Width: | Height: | Size: 748 B |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 2.8 KiB |
After Width: | Height: | Size: 2.3 KiB |
After Width: | Height: | Size: 1.5 KiB |
After Width: | Height: | Size: 3.7 KiB |
After Width: | Height: | Size: 1.1 KiB |
2
examples/wchess/wchess.wasm/chessboardjs-1.0.0/js/chessboard-1.0.0.min.js
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
# chessboard.js Change Log
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [1.0.0] - 2019-06-11
|
||||
- Orientation methods now return current orientation. [Issue #64]
|
||||
- Drop support for IE8
|
||||
- Do not check for `window.JSON` (Error #1004)
|
||||
- Rename `ChessBoard` to `Chessboard` (`ChessBoard` is still supported, however)
|
||||
- id query selectors are now supported as the first argument to `Chessboard()`
|
||||
- Remove Error #1002
|
||||
- Format code according to [StandardJS]
|
||||
- Bump minimum jQuery version to 1.8.3
|
||||
- Throttle piece drag functions
|
||||
|
||||
## [0.3.0] - 2013-08-10
|
||||
- Added `appearSpeed` animation config property
|
||||
- Added `onSnapbackEnd` event
|
||||
- Added `onMoveEnd` event
|
||||
|
||||
## [0.2.0] - 2013-08-05
|
||||
- Added `onMouseoverSquare` and `onMouseoutSquare` events
|
||||
- Added `onSnapEnd` event
|
||||
- Added square code as CSS class on the squares
|
||||
- Added [chess.js] integration examples
|
||||
|
||||
## [0.1.0] - 2013-05-21
|
||||
- Initial release
|
||||
|
||||
[chess.js]:https://github.com/jhlywa/chess.js
|
||||
[Issue #64]:https://github.com/oakmac/chessboardjs/issues/64
|
||||
[StandardJS]:https://standardjs.com/
|
@ -0,0 +1,20 @@
|
||||
Copyright 2019 Chris Oakman
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
@ -0,0 +1,82 @@
|
||||
# chessboard.js
|
||||
|
||||
chessboard.js is a JavaScript chessboard component. It depends on [jQuery].
|
||||
|
||||
Please see [chessboardjs.com] for documentation and examples.
|
||||
|
||||
## What is chessboard.js?
|
||||
|
||||
chessboard.js is a JavaScript chessboard component with a flexible "just a
|
||||
board" API that
|
||||
|
||||
chessboard.js is a standalone JavaScript Chess Board. It is designed to be "just
|
||||
a board" and expose a powerful API so that it can be used in different ways.
|
||||
Here's a non-exhaustive list of things you can do with chessboard.js:
|
||||
|
||||
- Use chessboard.js to show game positions alongside your expert commentary.
|
||||
- Use chessboard.js to have a tactics website where users have to guess the best
|
||||
move.
|
||||
- Integrate chessboard.js and [chess.js] with a PGN database and allow people to
|
||||
search and playback games (see [Example 5000])
|
||||
- Build a chess server and have users play their games out using the
|
||||
chessboard.js board.
|
||||
|
||||
chessboard.js is flexible enough to handle any of these situations with relative
|
||||
ease.
|
||||
|
||||
## What can chessboard.js **not** do?
|
||||
|
||||
The scope of chessboard.js is limited to "just a board." This is intentional and
|
||||
makes chessboard.js flexible for handling a multitude of chess-related problems.
|
||||
|
||||
This is a common source of confusion for new users. [remove?]
|
||||
|
||||
Specifically, chessboard.js does not understand anything about how the game of
|
||||
chess is played: how a knight moves, who's turn is it, is White in check?, etc.
|
||||
|
||||
Fortunately, the powerful [chess.js] library deals with exactly this sort of
|
||||
problem domain and plays nicely with chessboard.js's flexible API. Some examples
|
||||
of chessboard.js combined with chess.js: 5000, 5001, 5002
|
||||
|
||||
Please see the powerful [chess.js] library for an API to deal with these sorts
|
||||
of questions.
|
||||
|
||||
|
||||
This logic is distinct from the logic of the board. Please see the powerful
|
||||
[chess.js] library for this aspect of your application.
|
||||
|
||||
|
||||
|
||||
Here is a list of things that chessboard.js is **not**:
|
||||
|
||||
- A chess engine
|
||||
- A legal move validator
|
||||
- A PGN parser
|
||||
|
||||
chessboard.js is designed to work well with any of those things, but the idea
|
||||
behind chessboard.js is that the logic that controls the board should be
|
||||
independent of those other problems.
|
||||
|
||||
## Docs and Examples
|
||||
|
||||
- Docs - <http://chessboardjs.com/docs>
|
||||
- Examples - <http://chessboardjs.com/examples>
|
||||
|
||||
## Developer Tools
|
||||
|
||||
```sh
|
||||
# create a build in the build/ directory
|
||||
npm run build
|
||||
|
||||
# re-build the website
|
||||
npm run website
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
[MIT License](LICENSE.md)
|
||||
|
||||
[jQuery]:https://jquery.com/
|
||||
[chessboardjs.com]:http://chessboardjs.com
|
||||
[chess.js]:https://github.com/jhlywa/chess.js
|
||||
[Example 5000]:http://chessboardjs.com/examples#5000
|
@ -0,0 +1,29 @@
|
||||
{
|
||||
"author": "Chris Oakman <chris@oakmac.com> (http://chrisoakman.com/)",
|
||||
"name": "@chrisoakman/chessboardjs",
|
||||
"description": "JavaScript chessboard widget",
|
||||
"homepage": "https://chessboardjs.com",
|
||||
"license": "MIT",
|
||||
"version": "1.0.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git://github.com/oakmac/chessboardjs.git"
|
||||
},
|
||||
"files": ["dist/"],
|
||||
"dependencies": {
|
||||
"jquery": ">=3.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"csso": "3.5.1",
|
||||
"fs-plus": "3.1.1",
|
||||
"kidif": "1.1.0",
|
||||
"mustache": "2.3.0",
|
||||
"standard": "10.0.2",
|
||||
"uglify-js": "3.6.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "standard lib/chessboard.js && node scripts/build.js",
|
||||
"standard": "standard --fix lib/*.js website/js/*.js",
|
||||
"website": "node scripts/website.js"
|
||||
}
|
||||
}
|
499
examples/wchess/wchess.wasm/index-tmpl.html
Normal file
@ -0,0 +1,499 @@
|
||||
<!doctype html>
|
||||
<html lang="en-us">
|
||||
<head>
|
||||
<title>wchess : voice-controlled chess using Whisper + WebAssembly</title>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/iframe-resizer/4.3.1/iframeResizer.contentWindow.min.js"></script>
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=0.7, maximum-scale=1, minimum-scale=0.7, user-scalable=no"/>
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
|
||||
<style>
|
||||
#output {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0 auto;
|
||||
margin-top: 10px;
|
||||
border-left: 0px;
|
||||
border-right: 0px;
|
||||
padding-left: 0px;
|
||||
padding-right: 0px;
|
||||
display: block;
|
||||
background-color: black;
|
||||
color: white;
|
||||
font-size: 10px;
|
||||
font-family: 'Lucida Console', Monaco, monospace;
|
||||
outline: none;
|
||||
white-space: pre;
|
||||
overflow-wrap: normal;
|
||||
overflow-x: scroll;
|
||||
}
|
||||
.button {
|
||||
background-color: #000000;
|
||||
color: #FFFFFF;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
-moz-border-radius: 10px;
|
||||
-webkit-border-radius: 10px;
|
||||
margin:10px;
|
||||
width: 100px;
|
||||
height: 50px;
|
||||
-webkit-touch-callout: none; /* Safari */
|
||||
-webkit-user-select: none; /* Chrome */
|
||||
-moz-user-select: none; /* Firefox */
|
||||
-ms-user-select: none; /* Internet Explorer/Edge */
|
||||
user-select: none;
|
||||
}
|
||||
button[disabled]{
|
||||
background-color: #cccccc;
|
||||
color: #666666;
|
||||
padding: 20px;
|
||||
border-radius: 10px;
|
||||
-moz-border-radius: 10px;
|
||||
-webkit-border-radius: 10px;
|
||||
margin:10px;
|
||||
width: 100px;
|
||||
}
|
||||
.center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 500px;
|
||||
}
|
||||
#description {
|
||||
width: 500px;
|
||||
}
|
||||
</style>
|
||||
<link rel="stylesheet" href="css/chessboard-1.0.0.min.css" integrity="sha384-q94+BZtLrkL1/ohfjR8c6L+A6qzNH9R2hBLwyoAfu3i/WCvQjzL2RQJ3uNHDISdU" crossorigin="anonymous">
|
||||
</head>
|
||||
<body>
|
||||
<div id="main-container">
|
||||
<div id="description">
|
||||
<b>wchess : voice-controlled chess using Whisper + WebAssembly</b>
|
||||
|
||||
<br><br>
|
||||
|
||||
This is a demonstration of using Whisper to recognize voice commands in the browser.
|
||||
|
||||
<br><br>
|
||||
|
||||
Usage:<br>
|
||||
|
||||
<ul>
|
||||
<li>Select a Whisper model</li>
|
||||
<li>Accept the microphone permission request if prompted</li>
|
||||
<li>Hold the button and say a chess move (e.g. "Knight to c3")</li>
|
||||
<li>Release the button and wait for the move to be recognized</li>
|
||||
<li>Repeat</li>
|
||||
</ul>
|
||||
|
||||
Examples:<br>
|
||||
|
||||
<ul>
|
||||
<li><b>"d4"</b></li>
|
||||
<li><b>"e2 e4"</b></li>
|
||||
<li><b>"Knight f3"</b></li>
|
||||
<li><b>"Bishop to b5"</b></li>
|
||||
</ul>
|
||||
|
||||
Features:<br>
|
||||
|
||||
<ul>
|
||||
<li>Model quantization for reduced memory footprint (~42MB)</li>
|
||||
<li><a href="https://github.com/ggerganov/whisper.cpp/pull/1229">Grammar-based sampling</a> for improved recognition accuracy</li>
|
||||
</ul>
|
||||
|
||||
<b>
|
||||
Note that not all chess moves are supported. For example, castling and pawn promotion
|
||||
currently do not work, but can be easily implemented. There could also be some bugs in
|
||||
the move handling logic in general. The main reason for that is to keep the implementation
|
||||
simple. The assumption is that a real application would already have a proper move
|
||||
validation logic in place.<br><br>
|
||||
|
||||
The main purpose of this example is to demonstrate the capabilities of whisper.cpp and
|
||||
its application in the browser for voice recognition locally on your device.
|
||||
</b>
|
||||
|
||||
<br><br>
|
||||
|
||||
You can find more about this project on <a href="https://github.com/ggerganov/whisper.cpp/tree/master/examples/wchess">GitHub</a>.
|
||||
|
||||
<br><br>
|
||||
|
||||
<b>More examples:</b>
|
||||
<a href="https://whisper.ggerganov.com/">main</a> |
|
||||
<a href="https://whisper.ggerganov.com/bench">bench</a> |
|
||||
<a href="https://whisper.ggerganov.com/stream">stream</a> |
|
||||
<a href="https://whisper.ggerganov.com/command">command</a> |
|
||||
<a href="https://whisper.ggerganov.com/talk">talk</a> |
|
||||
|
||||
<br><br>
|
||||
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<div id="model-whisper">
|
||||
Whisper model: <span id="model-whisper-status"></span>
|
||||
<button id="fetch-whisper-tiny-en" onclick="loadWhisper()">tiny.en (Q8_0, 42 MB)</button>
|
||||
<span id="fetch-whisper-progress"></span>
|
||||
<br><br>
|
||||
<button id="clear" onclick="clearCache()">Clear browser cache</button>
|
||||
<!--
|
||||
<input type="file" id="file" name="file" onchange="loadFile(event, 'whisper.bin')" />
|
||||
-->
|
||||
</div>
|
||||
|
||||
<div id="game">
|
||||
<br>
|
||||
<div id="chessboard" style="width: 500px"></div>
|
||||
<script src="js/jquery-3.7.1.min.js"></script>
|
||||
<script src="js/chessboard-1.0.0.min.js"></script>
|
||||
<script>
|
||||
var board = Chessboard('chessboard', 'start')
|
||||
var move_count = 0;
|
||||
</script>
|
||||
|
||||
<br>
|
||||
|
||||
<div id="state">
|
||||
Status: <b><span id="state-status">select model</span></b>
|
||||
|
||||
<div id="input" class="center">
|
||||
<button id="toggler" class="button" onselectstart="return false" style="display: none">Hold</button>
|
||||
</div>
|
||||
|
||||
<pre id="state-grammar">[The grammar will be displayed here]</pre>
|
||||
|
||||
<pre id="state-moves">[The moves will be displayed here]</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
Debug output:
|
||||
<textarea id="output" rows="20"></textarea>
|
||||
|
||||
<br>
|
||||
|
||||
<b>Troubleshooting</b>
|
||||
|
||||
<br><br>
|
||||
|
||||
The page does some heavy computations, so make sure:
|
||||
|
||||
<ul>
|
||||
<li>To use a modern web browser (e.g. Chrome, Firefox)</li>
|
||||
<li>Your browser supports WASM <a href="https://webassembly.org/roadmap/">Fixed-width SIMD</a></li>
|
||||
</ul>
|
||||
|
||||
<div class="cell-version">
|
||||
<span>
|
||||
|
|
||||
Build time: <span class="nav-link">@GIT_DATE@</span> |
|
||||
Commit hash: <a class="nav-link" href="https://github.com/ggerganov/whisper.cpp/commit/@GIT_SHA1@">@GIT_SHA1@</a> |
|
||||
Commit subject: <span class="nav-link">@GIT_COMMIT_SUBJECT@</span> |
|
||||
<a class="nav-link" href="https://github.com/ggerganov/whisper.cpp/tree/master/examples/command.wasm">Source Code</a> |
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" src="js/helpers.js"></script>
|
||||
<script type='text/javascript'>
|
||||
// web audio context
|
||||
var context = null;
|
||||
|
||||
// the command instance
|
||||
var instance = null;
|
||||
|
||||
// model name
|
||||
var model_whisper = null;
|
||||
var model_file = null;
|
||||
|
||||
var module_ready = null;
|
||||
|
||||
var Module = {
|
||||
print: printTextarea,
|
||||
printErr: printTextarea,
|
||||
setStatus: function(text) {
|
||||
printTextarea('js: ' + text);
|
||||
},
|
||||
monitorRunDependencies: function(left) {
|
||||
},
|
||||
preRun: function() {
|
||||
printTextarea('js: Preparing ...');
|
||||
},
|
||||
postRun: function() {
|
||||
printTextarea('js: Module initialized successfully!');
|
||||
module_ready = true;
|
||||
initInstance();
|
||||
}
|
||||
};
|
||||
|
||||
function initInstance() {
|
||||
if (!module_ready || !model_file || instance) return
|
||||
|
||||
instance = Module.init(model_file);
|
||||
|
||||
if (instance) {
|
||||
setStatus('Ready');
|
||||
printTextarea("js: whisper initialized, instance: " + instance);
|
||||
}
|
||||
else {
|
||||
printTextarea("js: failed to initialize whisper");
|
||||
}
|
||||
}
|
||||
|
||||
function setStatus(text) {
|
||||
document.getElementById('state-status').innerHTML = text;
|
||||
}
|
||||
|
||||
//
|
||||
// fetch models
|
||||
//
|
||||
|
||||
let dbVersion = 1
|
||||
let dbName = 'whisper.ggerganov.com';
|
||||
let indexedDB = window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB
|
||||
|
||||
function storeFS(fname, buf) {
|
||||
// write to WASM file using FS_createDataFile
|
||||
// if the file exists, delete it
|
||||
try {
|
||||
Module.FS_unlink(fname);
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
|
||||
Module.FS_createDataFile("/", fname, buf, true, true);
|
||||
|
||||
printTextarea('storeFS: stored model: ' + fname + ' size: ' + buf.length);
|
||||
|
||||
document.getElementById('model-whisper-status').innerHTML = 'loaded "' + model_whisper + '"!';
|
||||
|
||||
model_file = fname;
|
||||
initInstance();
|
||||
}
|
||||
|
||||
function loadWhisper() {
|
||||
setStatus('Loading')
|
||||
//let url = 'https://whisper.ggerganov.com/ggml-model-whisper-tiny.en-q8_0.bin';
|
||||
let url = 'https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.en-q8_0.bin';
|
||||
let dst = 'whisper.bin';
|
||||
let size_mb = 42;
|
||||
|
||||
model_whisper = 'tiny.en-q8_0';
|
||||
|
||||
document.getElementById('model-whisper-status').innerHTML = 'loading "' + model_whisper + '" ... ';
|
||||
document.getElementById('fetch-whisper-tiny-en').style.display = 'none';
|
||||
|
||||
cbProgress = function(p) {
|
||||
let el = document.getElementById('fetch-whisper-progress');
|
||||
el.innerHTML = Math.round(100*p) + '%';
|
||||
};
|
||||
|
||||
cbCancel = function() {
|
||||
var el;
|
||||
el = document.getElementById('model-whisper-status'); if (el) el.innerHTML = '';
|
||||
};
|
||||
|
||||
loadRemote(url, dst, size_mb, cbProgress, storeFS, cbCancel, printTextarea);
|
||||
|
||||
// init audio capture so that the user receives a permission request
|
||||
{
|
||||
let context = new AudioContext({
|
||||
sampleRate: 16000,
|
||||
channelCount: 1,
|
||||
echoCancellation: false,
|
||||
autoGainControl: true,
|
||||
noiseSuppression: true,
|
||||
});
|
||||
navigator.mediaDevices.getUserMedia({audio: true, video: false})
|
||||
.then(function(s) {
|
||||
stream = s;
|
||||
stream.getTracks().forEach(function(track) {
|
||||
track.stop();
|
||||
});
|
||||
})
|
||||
.catch(function(err) {
|
||||
printTextarea('js: error getting audio stream: ' + err);
|
||||
});
|
||||
context.close();
|
||||
}
|
||||
|
||||
document.getElementById('toggler').style.display = 'block';
|
||||
}
|
||||
|
||||
//
|
||||
// microphone
|
||||
//
|
||||
|
||||
const kSampleRate = 16000;
|
||||
const kRestartRecording_s = 120;
|
||||
const kIntervalAudio_ms = 250; // pass the recorded audio to the C++ instance at this rate
|
||||
|
||||
var mediaRecorder = null;
|
||||
var doRecording = false;
|
||||
var startTime = 0;
|
||||
|
||||
window.AudioContext = window.AudioContext || window.webkitAudioContext;
|
||||
window.OfflineAudioContext = window.OfflineAudioContext || window.webkitOfflineAudioContext;
|
||||
|
||||
function stopRecording() {
|
||||
if (mediaRecorder) {
|
||||
mediaRecorder.stop();
|
||||
}
|
||||
}
|
||||
|
||||
function startRecording() {
|
||||
if (!context) {
|
||||
context = new AudioContext({
|
||||
sampleRate: kSampleRate,
|
||||
channelCount: 1,
|
||||
echoCancellation: false,
|
||||
autoGainControl: true,
|
||||
noiseSuppression: true,
|
||||
});
|
||||
}
|
||||
|
||||
startTime = Date.now();
|
||||
|
||||
var chunks = [];
|
||||
var stream = null;
|
||||
|
||||
navigator.mediaDevices.getUserMedia({audio: true, video: false})
|
||||
.then(function(s) {
|
||||
stream = s;
|
||||
mediaRecorder = new MediaRecorder(stream);
|
||||
mediaRecorder.ondataavailable = function(e) {
|
||||
chunks.push(e.data);
|
||||
|
||||
var blob = new Blob(chunks, { 'type' : 'audio/ogg; codecs=opus' });
|
||||
var reader = new FileReader();
|
||||
|
||||
reader.onload = function(event) {
|
||||
var buf = new Uint8Array(reader.result);
|
||||
context.decodeAudioData(buf.buffer, function(audioBuffer) {
|
||||
var offlineContext = new OfflineAudioContext(audioBuffer.numberOfChannels, audioBuffer.length, audioBuffer.sampleRate);
|
||||
var source = offlineContext.createBufferSource();
|
||||
source.buffer = audioBuffer;
|
||||
source.connect(offlineContext.destination);
|
||||
source.start(0);
|
||||
|
||||
offlineContext.startRendering().then(function(renderedBuffer) {
|
||||
let audio = renderedBuffer.getChannelData(0);
|
||||
printTextarea('js: number of samples: ' + audio.length);
|
||||
Module.set_audio(instance, audio);
|
||||
});
|
||||
|
||||
mediaRecorder = null;
|
||||
context = null;
|
||||
});
|
||||
}
|
||||
|
||||
reader.readAsArrayBuffer(blob);
|
||||
};
|
||||
|
||||
mediaRecorder.onstop = function(e) {
|
||||
stream.getTracks().forEach(function(track) {
|
||||
track.stop();
|
||||
});
|
||||
};
|
||||
|
||||
mediaRecorder.start();
|
||||
})
|
||||
.catch(function(err) {
|
||||
printTextarea('js: error getting audio stream: ' + err);
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// main
|
||||
//
|
||||
|
||||
var nLines = 0;
|
||||
var movesAll = '';
|
||||
|
||||
// document.body.addEventListener('keydown', function(event) {
|
||||
// if (event.keyCode === 32) {
|
||||
// document.getElementById('toggler').innerText = "";
|
||||
// onStart();
|
||||
// }
|
||||
// }, true);
|
||||
|
||||
// document.body.addEventListener('keyup', function(event) {
|
||||
// if (event.keyCode === 32) {
|
||||
// document.getElementById('toggler').innerText = "Hold";
|
||||
// onStop();
|
||||
// }
|
||||
// }, true);
|
||||
|
||||
document.getElementById('toggler').addEventListener("touchstart", function(event){
|
||||
this.innerText = "";
|
||||
onStart();
|
||||
}, true);
|
||||
|
||||
document.getElementById('toggler').addEventListener("touchend", function(event){
|
||||
this.innerText = "Hold";
|
||||
onStop();
|
||||
}, true)
|
||||
|
||||
document.getElementById('toggler').addEventListener('mousedown', function(event) {
|
||||
this.innerText = "";
|
||||
onStart();
|
||||
}, true);
|
||||
|
||||
document.getElementById('toggler').addEventListener('mouseup', function(event) {
|
||||
this.innerText = "Hold";
|
||||
onStop();
|
||||
}, true);
|
||||
|
||||
function onStart() {
|
||||
if (!instance) return;
|
||||
setStatus('Listening');
|
||||
|
||||
startRecording();
|
||||
}
|
||||
|
||||
function onStop() {
|
||||
setStatus('Processing');
|
||||
printTextarea('js: stopping recording ...');
|
||||
stopRecording();
|
||||
}
|
||||
|
||||
function setMove(move, prob) {
|
||||
if (move != null && move.length > 1) {
|
||||
let gameOver = move[move.length - 1] === '#';
|
||||
if (gameOver) {
|
||||
move = move.substring(0, move.length - 1);
|
||||
document.getElementById('toggler').disabled = true;
|
||||
}
|
||||
board.move(move);
|
||||
|
||||
movesAll += move + ', prob = ' + prob.toFixed(2) + '% <br>';
|
||||
nLines++;
|
||||
|
||||
// if more than 10 lines, remove the first line
|
||||
if (nLines > 10) {
|
||||
var i = movesAll.indexOf('<br>');
|
||||
if (i > 0) {
|
||||
movesAll = movesAll.substring(i + 4);
|
||||
nLines--;
|
||||
}
|
||||
}
|
||||
++move_count;
|
||||
setStatus(gameOver ? 'Done' : move_count % 2 ? 'Black\'s turn' : 'White\'s turn');
|
||||
document.getElementById('state-moves').innerHTML = movesAll;
|
||||
}
|
||||
else {
|
||||
setStatus('Failed. ' + (move_count % 2 ? 'Black\'s turn' : 'White\'s turn'));
|
||||
}
|
||||
}
|
||||
|
||||
function setGrammar(grammar) {
|
||||
document.getElementById('state-grammar').innerHTML = grammar;
|
||||
}
|
||||
|
||||
</script>
|
||||
<script type="text/javascript" src="js/chess.js"></script>
|
||||
</body>
|
||||
</html>
|
2
examples/wchess/wchess.wasm/jquery-3.7.1.min.js
vendored
Normal file
141
examples/wchess/wchess.wasm/wchess.wasm.cpp
Normal file
@ -0,0 +1,141 @@
|
||||
#include <WChess.h>
|
||||
#include <emscripten.h>
|
||||
#include <emscripten/bind.h>
|
||||
|
||||
#include <thread>
|
||||
|
||||
constexpr int N_THREAD = 8;
|
||||
|
||||
std::vector<struct whisper_context *> g_contexts(4, nullptr);
|
||||
|
||||
std::mutex g_mutex;
|
||||
std::thread g_worker;
|
||||
|
||||
std::condition_variable g_cv;
|
||||
|
||||
bool g_running(false);
|
||||
std::vector<float> g_pcmf32;
|
||||
|
||||
void set_move(const std::string & move, float prob) {
|
||||
MAIN_THREAD_EM_ASM({
|
||||
setMove(UTF8ToString($0), $1)
|
||||
}, move.c_str(), prob);
|
||||
}
|
||||
|
||||
void set_grammar(const std::string & grammar) {
|
||||
MAIN_THREAD_EM_ASM({
|
||||
setGrammar(UTF8ToString($0))
|
||||
}, grammar.c_str());
|
||||
}
|
||||
|
||||
bool get_audio(std::vector<float> & audio) {
|
||||
std::unique_lock<std::mutex> lock(g_mutex);
|
||||
g_cv.wait(lock, [] { return !g_running || !g_pcmf32.empty(); });
|
||||
if (!g_running) return false;
|
||||
audio = std::move(g_pcmf32);
|
||||
return true;
|
||||
}
|
||||
|
||||
void wchess_main(size_t i) {
|
||||
struct whisper_full_params wparams = whisper_full_default_params(whisper_sampling_strategy::WHISPER_SAMPLING_GREEDY);
|
||||
|
||||
wparams.n_threads = std::min(N_THREAD, (int) std::thread::hardware_concurrency());
|
||||
wparams.offset_ms = 0;
|
||||
wparams.translate = false;
|
||||
wparams.no_context = true;
|
||||
wparams.single_segment = true;
|
||||
wparams.print_realtime = false;
|
||||
wparams.print_progress = false;
|
||||
wparams.print_timestamps = true;
|
||||
wparams.print_special = false;
|
||||
wparams.no_timestamps = true;
|
||||
|
||||
wparams.max_tokens = 32;
|
||||
wparams.audio_ctx = 1280; // partial encoder context for better performance
|
||||
|
||||
wparams.temperature = 0.0f;
|
||||
wparams.temperature_inc = 2.0f;
|
||||
wparams.greedy.best_of = 1;
|
||||
|
||||
wparams.beam_search.beam_size = 1;
|
||||
|
||||
wparams.language = "en";
|
||||
|
||||
wparams.grammar_penalty = 100.0;
|
||||
wparams.initial_prompt = "bishop to c3, rook to d4, knight to e5, d4 d5, knight to c3, c3, queen to d4, king b1, pawn to a1, bishop to b2, knight to c3,";
|
||||
|
||||
printf("command: using %d threads\n", wparams.n_threads);
|
||||
|
||||
WChess::callbacks cb;
|
||||
cb.get_audio = get_audio;
|
||||
cb.set_move = set_move;
|
||||
cb.set_grammar = set_grammar;
|
||||
|
||||
WChess(g_contexts[i], wparams, cb, {}).run();
|
||||
|
||||
if (i < g_contexts.size()) {
|
||||
whisper_free(g_contexts[i]);
|
||||
g_contexts[i] = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
EMSCRIPTEN_BINDINGS(command) {
|
||||
emscripten::function("init", emscripten::optional_override([](const std::string & path_model) {
|
||||
for (size_t i = 0; i < g_contexts.size(); ++i) {
|
||||
if (g_contexts[i] == nullptr) {
|
||||
g_contexts[i] = whisper_init_from_file_with_params(path_model.c_str(), whisper_context_default_params());
|
||||
if (g_contexts[i] != nullptr) {
|
||||
g_running = true;
|
||||
if (g_worker.joinable()) {
|
||||
g_worker.join();
|
||||
}
|
||||
g_worker = std::thread([i]() {
|
||||
wchess_main(i);
|
||||
});
|
||||
|
||||
return i + 1;
|
||||
} else {
|
||||
return (size_t) 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (size_t) 0;
|
||||
}));
|
||||
|
||||
emscripten::function("free", emscripten::optional_override([](size_t /* index */) {
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(g_mutex);
|
||||
g_running = false;
|
||||
}
|
||||
g_cv.notify_one();
|
||||
}));
|
||||
|
||||
emscripten::function("set_audio", emscripten::optional_override([](size_t index, const emscripten::val & audio) {
|
||||
--index;
|
||||
|
||||
if (index >= g_contexts.size()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (g_contexts[index] == nullptr) {
|
||||
return -2;
|
||||
}
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(g_mutex);
|
||||
const int n = audio["length"].as<int>();
|
||||
|
||||
emscripten::val heap = emscripten::val::module_property("HEAPU8");
|
||||
emscripten::val memory = heap["buffer"];
|
||||
|
||||
g_pcmf32.resize(n);
|
||||
|
||||
emscripten::val memoryView = audio["constructor"].new_(memory, reinterpret_cast<uintptr_t>(g_pcmf32.data()), n);
|
||||
memoryView.call<void>("set", audio);
|
||||
}
|
||||
g_cv.notify_one();
|
||||
|
||||
return 0;
|
||||
}));
|
||||
}
|