Compare commits

3 Commits

Author SHA1 Message Date
pleb 561ace9443 Limit voice selection to avoid awkward assignments
CI / macos-latest (push) Has been cancelled
CI / ubuntu-latest (push) Has been cancelled
CI / windows-latest (push) Has been cancelled
2026-07-28 18:08:10 -07:00
pleb 514fb832c7 Add logo svg
CI / macos-latest (push) Has been cancelled
CI / ubuntu-latest (push) Has been cancelled
CI / windows-latest (push) Has been cancelled
2026-07-21 11:02:24 -07:00
pleb d7527a9319 Add nix flake
CI / macos-latest (push) Has been cancelled
CI / ubuntu-latest (push) Has been cancelled
CI / windows-latest (push) Has been cancelled
2026-07-21 10:53:33 -07:00
5 changed files with 220 additions and 16 deletions
+9 -15
View File
@@ -1,12 +1,8 @@
# Mudmouth # Mudmouth
```text <p align="center">
_ _ _ <img src="docs/mudmouth-logo.svg" alt="Mudmouth — The log awakens; the dungeon speaks" width="660">
_ __ ___ _ _ __| |_ __ ___ ___ | | | |_ ____ </p>
| '_ ` _ \| | | |/ _` | '_ ` _ \ / _ \| |_| | '_ /
| | | | | | |_| | (_| | | | | | | (_) | _ | | | |
|_| |_| |_|\__,_|\__,_|_| |_| |_|\___/|_| |_|_| |_|
```
Mudmouth is a cross-platform, classic-MUD-inspired TUI for EverQuest Legends. Mudmouth is a cross-platform, classic-MUD-inspired TUI for EverQuest Legends.
It follows an EverQuest log, gives NPC dialogue a Kokoro-FastAPI voice, and It follows an EverQuest log, gives NPC dialogue a Kokoro-FastAPI voice, and
@@ -22,12 +18,14 @@ speaker mappings, generated audio, and TTS endpoint stay on your computer.
- Watches an EverQuest log without replaying historical lines on startup. - Watches an EverQuest log without replaying historical lines on startup.
- Speaks `Name says, '…'` NPC dialogue by default. - Speaks `Name says, '…'` NPC dialogue by default.
- Calls `GET /health`, shows live Kokoro connectivity in the status bar, and - Calls `GET /health`, shows live Kokoro connectivity in the status bar, and
refreshes its selectable voice catalog from `GET /v1/audio/voices`. refreshes its selectable voice catalog from `GET /v1/audio/voices`. NPC
selection is limited to Mudmouth's curated 27-voice English pool.
- Sends non-streaming WAV requests to `POST /v1/audio/speech` and plays clips - Sends non-streaming WAV requests to `POST /v1/audio/speech` and plays clips
sequentially with native audio support. sequentially with native audio support.
- Repeats an NPC's identical line at 1.5x speed, then 2x speed when each repeat - Repeats an NPC's identical line at 1.5x speed, then 2x speed when each repeat
arrives within 15 seconds; a later line returns to normal speed. arrives within 15 seconds; a later line returns to normal speed.
- Picks a real voice from your Kokoro server for a newly seen NPC and saves it. - Picks a curated voice available on your Kokoro server for a newly seen NPC
and saves it.
- Lets you pin an NPC to a particular voice, reroll a persistent random voice, - Lets you pin an NPC to a particular voice, reroll a persistent random voice,
or set the NPC to get a new random voice for every line. or set the NPC to get a new random voice for every line.
- Can optionally narrate zone entries (`You have entered Lake Rathetear.`). - Can optionally narrate zone entries (`You have entered Lake Rathetear.`).
@@ -35,14 +33,10 @@ speaker mappings, generated audio, and TTS endpoint stay on your computer.
## Requirements ## Requirements
- A reachable [Kokoro-FastAPI](docs/kokoro-fastapi.json) instance. Install and - A reachable [Kokoro-FastAPI](https://github.com/remsky/Kokoro-FastAPI) instance. Install and
run it using the upstream [Kokoro-FastAPI instructions](https://github.com/remsky/Kokoro-FastAPI); run it using the upstream instructions; Mudmouth uses `http://127.0.0.1:8880` by default.
Mudmouth uses `http://127.0.0.1:8880` by default.
- An EverQuest log with `/log on`. - An EverQuest log with `/log on`.
No `ffplay`, `mpv`, PipeWire command, or other external audio player is
required; Mudmouth plays returned WAV audio itself.
## Build and run ## Build and run
See [development and build notes](docs/development.md) for local setup, See [development and build notes](docs/development.md) for local setup,
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 37 KiB

Generated
+27
View File
@@ -0,0 +1,27 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1784497964,
"narHash": "sha256-vlHUuqAcbcH2RKmHbPiuQzbv1pnzzavXnI62RD0bqCU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "241313f4e8e508cb9b13278c2b0fa25b9ca27163",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}
+68
View File
@@ -0,0 +1,68 @@
{
description = "Mudmouth, a classic-MUD-inspired EverQuest log reader";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs =
{ self, nixpkgs }:
let
systems = [
"x86_64-linux"
"aarch64-linux"
];
forAllSystems = nixpkgs.lib.genAttrs systems;
in
{
packages = forAllSystems (
system:
let
pkgs = import nixpkgs { inherit system; };
in
{
default = pkgs.rustPlatform.buildRustPackage {
pname = "mudmouth";
version = "0.0.2";
src = self;
cargoLock.lockFile = ./Cargo.lock;
nativeBuildInputs = [ pkgs.pkg-config ];
buildInputs = [ pkgs.alsa-lib ];
meta = with pkgs.lib; {
description = "A classic-MUD-inspired EverQuest log reader with Kokoro voices";
homepage = "https://github.com/pleb/mudmouth";
license = licenses.mit;
mainProgram = "mudmouth";
platforms = platforms.linux;
};
};
}
);
apps = forAllSystems (system: {
default = {
type = "app";
program = "${self.packages.${system}.default}/bin/mudmouth";
};
});
devShells = forAllSystems (
system:
let
pkgs = import nixpkgs { inherit system; };
in
{
default = pkgs.mkShell {
packages = with pkgs; [
cargo
rustc
rustfmt
clippy
pkg-config
alsa-lib
];
};
}
);
};
}
+36 -1
View File
@@ -3,6 +3,40 @@ use reqwest::Url;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::time::Duration; use std::time::Duration;
/// Curated English Kokoro voices used for NPC selection and the voice manager.
///
/// Keeping this allowlist here means a server update cannot unexpectedly add
/// voices to Mudmouth's random assignment pool.
const NPC_VOICE_ALLOWLIST: &[&str] = &[
"af_heart",
"af_bella",
"af_nicole",
"af_alloy",
"af_aoede",
"af_kore",
"af_nova",
"af_sarah",
"af_river",
"af_jessica",
"af_sky",
"am_michael",
"am_fenrir",
"am_eric",
"am_echo",
"am_liam",
"am_onyx",
"am_adam",
"am_puck",
"bf_alice",
"bf_emma",
"bf_isabella",
"bf_lily",
"bm_daniel",
"bm_fable",
"bm_george",
"bm_lewis",
];
#[derive(Clone)] #[derive(Clone)]
pub struct KokoroClient { pub struct KokoroClient {
client: reqwest::Client, client: reqwest::Client,
@@ -80,6 +114,7 @@ impl KokoroClient {
.into_voices() .into_voices()
.into_iter() .into_iter()
.map(|voice| voice.id) .map(|voice| voice.id)
.filter(|voice| NPC_VOICE_ALLOWLIST.contains(&voice.as_str()))
.collect()) .collect())
} }
@@ -136,7 +171,7 @@ mod tests {
let url = serve_once( let url = serve_once(
"200 OK", "200 OK",
"application/json", "application/json",
br#"[{"id":"af_heart","name":"Heart"},{"id":"am_adam","name":"Adam"}]"#, br#"[{"id":"af_heart","name":"Heart"},{"id":"am_adam","name":"Adam"},{"id":"af_onyx","name":"Onyx"}]"#,
); );
let voices = KokoroClient::new(&url).unwrap().voices().await.unwrap(); let voices = KokoroClient::new(&url).unwrap().voices().await.unwrap();
assert_eq!(voices, vec!["af_heart", "am_adam"]); assert_eq!(voices, vec!["af_heart", "am_adam"]);