I'm a bit lost on this commit.. BUT it contains things along the sounds of
- Modified transcription engine - A RESTful API for the companion VIVO-terminal application - Kokoro voice engine
This commit is contained in:
Generated
+1
-1
@@ -7,7 +7,7 @@
|
|||||||
<component name="FrameworkDetectionExcludesConfiguration">
|
<component name="FrameworkDetectionExcludesConfiguration">
|
||||||
<file type="web" url="file://$PROJECT_DIR$" />
|
<file type="web" url="file://$PROJECT_DIR$" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" languageLevel="JDK_26" default="true" project-jdk-name="26" project-jdk-type="JavaSDK">
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_25" default="true" project-jdk-name="graalvm-25" project-jdk-type="JavaSDK">
|
||||||
<output url="file://$PROJECT_DIR$/out" />
|
<output url="file://$PROJECT_DIR$/out" />
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
+33
-4
@@ -1,6 +1,11 @@
|
|||||||
|
import com.github.jengelman.gradle.plugins.shadow.transformers.AppendingTransformer
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id 'java'
|
id 'java'
|
||||||
id 'com.gradleup.shadow' version '9.0.0-beta7'
|
id 'com.gradleup.shadow' version '9.0.0-beta7'
|
||||||
|
|
||||||
|
id 'org.springframework.boot' version '4.1.0-M4'
|
||||||
|
id 'io.spring.dependency-management' version '1.1.4'
|
||||||
}
|
}
|
||||||
|
|
||||||
group = 'me.zacharias'
|
group = 'me.zacharias'
|
||||||
@@ -15,10 +20,16 @@ dependencies {
|
|||||||
|
|
||||||
implementation('me.neurodock:Core:+');
|
implementation('me.neurodock:Core:+');
|
||||||
implementation('io.github.jvoice-project:piper-jni:+')
|
implementation('io.github.jvoice-project:piper-jni:+')
|
||||||
implementation("org.jetbrains:annotations:23.1.0")
|
implementation('com.microsoft.onnxruntime:onnxruntime:1.27.0')
|
||||||
implementation("io.github.givimad:whisper-jni:+")
|
implementation('org.jetbrains:annotations:23.1.0')
|
||||||
implementation 'com.fasterxml.jackson.core:jackson-databind:2.17.2'
|
implementation('com.fasterxml.jackson.core:jackson-databind:2.17.2')
|
||||||
implementation 'com.vdurmont:emoji-java:5.1.1'
|
implementation('com.vdurmont:emoji-java:5.1.1')
|
||||||
|
|
||||||
|
implementation('info.picocli:picocli:4.7.7')
|
||||||
|
|
||||||
|
implementation('org.springframework.boot:spring-boot-starter-web:4.1.0-M4')
|
||||||
|
implementation('org.springframework.boot:spring-boot-starter-webflux:4.1.0-M4')
|
||||||
|
implementation('org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.0-M1')
|
||||||
|
|
||||||
testImplementation platform('org.junit:junit-bom:6.0.0')
|
testImplementation platform('org.junit:junit-bom:6.0.0')
|
||||||
testImplementation 'org.junit.jupiter:junit-jupiter'
|
testImplementation 'org.junit.jupiter:junit-jupiter'
|
||||||
@@ -37,6 +48,24 @@ tasks.register('copyPythonRuntime', Copy) {
|
|||||||
into "${layout.buildDirectory.get()}/resources/main/python"
|
into "${layout.buildDirectory.get()}/resources/main/python"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tasks.shadowJar {
|
||||||
|
mergeServiceFiles() // merges META-INF/services/*
|
||||||
|
|
||||||
|
transform(AppendingTransformer) {
|
||||||
|
resource = "META-INF/spring.handlers"
|
||||||
|
}
|
||||||
|
transform(AppendingTransformer) {
|
||||||
|
resource = "META-INF/spring.schemas"
|
||||||
|
}
|
||||||
|
transform(AppendingTransformer) {
|
||||||
|
resource = "META-INF/spring.factories"
|
||||||
|
}
|
||||||
|
// Spring Boot 2.4+ auto-configuration file, needs line-merging too
|
||||||
|
transform(AppendingTransformer) {
|
||||||
|
resource = "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
tasks.named('processResources') {
|
tasks.named('processResources') {
|
||||||
dependsOn 'copyPythonRuntime'
|
dependsOn 'copyPythonRuntime'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package me.neurodock.glados;
|
package me.neurodock.glados;
|
||||||
|
|
||||||
import me.neurodock.core.PrintAdvanceMessageHandler;
|
|
||||||
import me.neurodock.core.ToolCallingRender;
|
import me.neurodock.core.ToolCallingRender;
|
||||||
import me.neurodock.ollama.OllamaFunctionArgument;
|
import me.neurodock.ollama.OllamaFunctionArgument;
|
||||||
import me.neurodock.ollama.OllamaFunctionTool;
|
import me.neurodock.ollama.OllamaFunctionTool;
|
||||||
@@ -13,8 +12,8 @@ import java.util.Objects;
|
|||||||
|
|
||||||
public class AnnouncerVoiceCalling extends OllamaFunctionTool {
|
public class AnnouncerVoiceCalling extends OllamaFunctionTool {
|
||||||
|
|
||||||
Main.PlaybackHandler pb;
|
Main.PlaybackHandlerPiper pb;
|
||||||
public AnnouncerVoiceCalling(Main.PlaybackHandler printAdvanceMessageHandler) {
|
public AnnouncerVoiceCalling(Main.PlaybackHandlerPiper printAdvanceMessageHandler) {
|
||||||
this.pb = printAdvanceMessageHandler;
|
this.pb = printAdvanceMessageHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
package me.neurodock.glados;
|
||||||
|
|
||||||
|
import ai.onnxruntime.OnnxTensor;
|
||||||
|
import ai.onnxruntime.OrtEnvironment;
|
||||||
|
import ai.onnxruntime.OrtSession;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.ByteBuffer;
|
||||||
|
import java.nio.ByteOrder;
|
||||||
|
import java.nio.FloatBuffer;
|
||||||
|
import java.nio.LongBuffer;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
public class KokoroVoice implements AutoCloseable {
|
||||||
|
private static final int SAMPLE_RATE = 24000;
|
||||||
|
|
||||||
|
private final Map<String, Integer> VOCAB; // the table from before
|
||||||
|
private final OrtEnvironment env;
|
||||||
|
private final OrtSession session;
|
||||||
|
private final float[] styleData; // full contents of af_heart.bin
|
||||||
|
private static final LinkedHashMap<String, String> FROM_ESPEAK = new LinkedHashMap<>();
|
||||||
|
static {
|
||||||
|
// Longest keys first so e.g. "e^ɪ" is tried before the bare "e" fallback
|
||||||
|
FROM_ESPEAK.put("ʔˌn\u0329", "tᵊn");
|
||||||
|
FROM_ESPEAK.put("a^ɪ", "I");
|
||||||
|
FROM_ESPEAK.put("a^ʊ", "W");
|
||||||
|
FROM_ESPEAK.put("d^ʒ", "ʤ");
|
||||||
|
FROM_ESPEAK.put("e^ɪ", "A");
|
||||||
|
FROM_ESPEAK.put("t^ʃ", "ʧ");
|
||||||
|
FROM_ESPEAK.put("ɔ^ɪ", "Y");
|
||||||
|
FROM_ESPEAK.put("ə^l", "ᵊl");
|
||||||
|
FROM_ESPEAK.put("ʲO", "jO");
|
||||||
|
FROM_ESPEAK.put("ʲQ", "jQ");
|
||||||
|
FROM_ESPEAK.put("ʔn", "tᵊn");
|
||||||
|
FROM_ESPEAK.put("\u0303", "");
|
||||||
|
FROM_ESPEAK.put("e", "A");
|
||||||
|
FROM_ESPEAK.put("r", "ɹ");
|
||||||
|
FROM_ESPEAK.put("x", "k");
|
||||||
|
FROM_ESPEAK.put("ç", "k");
|
||||||
|
FROM_ESPEAK.put("ɐ", "ə");
|
||||||
|
FROM_ESPEAK.put("ɚ", "əɹ");
|
||||||
|
FROM_ESPEAK.put("ɬ", "l");
|
||||||
|
FROM_ESPEAK.put("ʔ", "t");
|
||||||
|
FROM_ESPEAK.put("ʲ", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
public KokoroVoice(Path modelOnnxPath, Path voiceBinPath, Path config) throws Exception {
|
||||||
|
env = OrtEnvironment.getEnvironment();
|
||||||
|
session = env.createSession(modelOnnxPath.toString(), new OrtSession.SessionOptions());
|
||||||
|
styleData = readFloatBin(voiceBinPath); // reshape logic below
|
||||||
|
try(BufferedReader reader = Files.newBufferedReader(config, StandardCharsets.UTF_8)){
|
||||||
|
StringBuilder data = new StringBuilder();
|
||||||
|
String line;
|
||||||
|
while((line = reader.readLine()) != null)
|
||||||
|
{
|
||||||
|
data.append(line);
|
||||||
|
}
|
||||||
|
JSONObject obj = new JSONObject(data.toString());
|
||||||
|
VOCAB = new HashMap<>();
|
||||||
|
for(String key : obj.getJSONObject("vocab").keySet())
|
||||||
|
{
|
||||||
|
VOCAB.put(key, obj.getJSONObject("vocab").getInt(key));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public KokoroVoice(Path modelOnnxPath, Path voiceBinPath) throws Exception {
|
||||||
|
this(modelOnnxPath, voiceBinPath, Path.of(modelOnnxPath.toFile().getPath()+".json"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSampleRate() { return SAMPLE_RATE; }
|
||||||
|
|
||||||
|
public short[] synthesize(String text) throws Exception {
|
||||||
|
String ipa = phonemize(text);
|
||||||
|
long[] tokenIds = tokenize(ipa);
|
||||||
|
float[] style = selectStyleVector(tokenIds.length); // af_heart.bin is length-indexed
|
||||||
|
|
||||||
|
try (OnnxTensor inputIds = OnnxTensor.createTensor(env,
|
||||||
|
LongBuffer.wrap(tokenIds), new long[]{1, tokenIds.length});
|
||||||
|
OnnxTensor styleTensor = OnnxTensor.createTensor(env,
|
||||||
|
FloatBuffer.wrap(style), new long[]{1, style.length});
|
||||||
|
OnnxTensor speed = OnnxTensor.createTensor(env, new float[]{1.0f})) {
|
||||||
|
|
||||||
|
Map<String, OnnxTensor> inputs = Map.of(
|
||||||
|
"input_ids", inputIds, "style", styleTensor, "speed", speed);
|
||||||
|
|
||||||
|
try (OrtSession.Result result = session.run(inputs)) {
|
||||||
|
float[][] audio = (float[][]) result.get(0).getValue();
|
||||||
|
return floatToPcm16(audio[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private float[] loadStyleData(Path binPath) throws IOException {
|
||||||
|
byte[] raw = Files.readAllBytes(binPath);
|
||||||
|
FloatBuffer fb = ByteBuffer.wrap(raw)
|
||||||
|
.order(ByteOrder.LITTLE_ENDIAN) // numpy float32 files are little-endian on x86
|
||||||
|
.asFloatBuffer();
|
||||||
|
float[] all = new float[fb.remaining()];
|
||||||
|
fb.get(all);
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float[] readFloatBin(Path binPath) throws IOException {
|
||||||
|
byte[] raw = Files.readAllBytes(binPath);
|
||||||
|
FloatBuffer fb = ByteBuffer.wrap(raw)
|
||||||
|
.order(ByteOrder.LITTLE_ENDIAN)
|
||||||
|
.asFloatBuffer();
|
||||||
|
float[] all = new float[fb.remaining()];
|
||||||
|
fb.get(all);
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
|
||||||
|
private float[] selectStyleVector(int tokenCount) {
|
||||||
|
int offset = tokenCount * 256;
|
||||||
|
if (offset + 256 > styleData.length) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"tokenCount " + tokenCount + " exceeds style vector table (max index "
|
||||||
|
+ (styleData.length / 256 - 1) + ") — sentence is too long, chunk it");
|
||||||
|
}
|
||||||
|
float[] style = new float[256];
|
||||||
|
System.arraycopy(styleData, offset, style, 0, 256);
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String phonemize(String text) throws IOException, InterruptedException {
|
||||||
|
Process p = new ProcessBuilder("espeak-ng", "-q", "--ipa", "-v", "en-us", text)
|
||||||
|
.redirectErrorStream(true).start();
|
||||||
|
String ipa = new String(p.getInputStream().readAllBytes(), StandardCharsets.UTF_8).trim().replaceAll("\\R+", " ");
|
||||||
|
p.waitFor();
|
||||||
|
//return fromEspeak(ipa);
|
||||||
|
return ipa;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String fromEspeak(String ps) {
|
||||||
|
for (var entry : FROM_ESPEAK.entrySet()) {
|
||||||
|
ps = ps.replace(entry.getKey(), entry.getValue());
|
||||||
|
}
|
||||||
|
// combining vowel-syllabicity mark (U+0329) -> ᵊ before the preceding consonant
|
||||||
|
ps = ps.replaceAll("(\\S)\u0329", "ᵊ$1").replace("\u0329", "");
|
||||||
|
|
||||||
|
// American-English-specific
|
||||||
|
ps = ps.replace("o^ʊ", "O")
|
||||||
|
.replace("ɜːɹ", "ɜɹ")
|
||||||
|
.replace("ɜː", "ɜɹ")
|
||||||
|
.replace("ɪə", "iə")
|
||||||
|
.replace("ː", "")
|
||||||
|
.replace("^", "");
|
||||||
|
return ps;
|
||||||
|
}
|
||||||
|
|
||||||
|
private long[] tokenize(String ipa) {
|
||||||
|
List<Long> ids = new ArrayList<>();
|
||||||
|
ids.add(0L);
|
||||||
|
for (char c : ipa.toCharArray()) {
|
||||||
|
Integer id = VOCAB.get(String.valueOf(c));
|
||||||
|
if (id != null) ids.add(id.longValue());
|
||||||
|
}
|
||||||
|
ids.add(0L);
|
||||||
|
return ids.stream().mapToLong(Long::longValue).toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static short[] floatToPcm16(float[] audio) {
|
||||||
|
short[] out = new short[audio.length];
|
||||||
|
for (int i = 0; i < audio.length; i++) {
|
||||||
|
float v = Math.max(-1f, Math.min(1f, audio[i]));
|
||||||
|
out[i] = (short) (v * 32767f);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws Exception {
|
||||||
|
session.close();
|
||||||
|
env.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,70 +3,241 @@ package me.neurodock.glados;
|
|||||||
import com.vdurmont.emoji.EmojiParser;
|
import com.vdurmont.emoji.EmojiParser;
|
||||||
import io.github.jvoiceproject.piperjni.PiperJNI;
|
import io.github.jvoiceproject.piperjni.PiperJNI;
|
||||||
import io.github.jvoiceproject.piperjni.PiperVoice;
|
import io.github.jvoiceproject.piperjni.PiperVoice;
|
||||||
import me.neurodock.core.Core;
|
import me.neurodock.core.*;
|
||||||
import me.neurodock.core.LLMSystemPrompt;
|
import me.neurodock.core.files.FileHandlerLocation;
|
||||||
import me.neurodock.core.LaunchOptions;
|
import me.neurodock.core.memory.CoreMemory;
|
||||||
import me.neurodock.core.PrintAdvanceMessageHandler;
|
import me.neurodock.glados.rest.APIApplication;
|
||||||
import me.neurodock.ollama.OllamaMessage;
|
import me.neurodock.ollama.*;
|
||||||
import me.neurodock.ollama.OllamaMessageRole;
|
|
||||||
import me.neurodock.ollama.OllamaObject;
|
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
import picocli.CommandLine;
|
||||||
|
import picocli.CommandLine.Option;
|
||||||
|
import picocli.CommandLine.Command;
|
||||||
|
|
||||||
import javax.sound.sampled.AudioFormat;
|
|
||||||
import javax.sound.sampled.AudioSystem;
|
|
||||||
import javax.sound.sampled.LineUnavailableException;
|
import javax.sound.sampled.LineUnavailableException;
|
||||||
import javax.sound.sampled.TargetDataLine;
|
import java.io.BufferedReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
public class Main {
|
import static me.neurodock.core.Core.writeLog;
|
||||||
|
|
||||||
|
@Command(name = "vivo-host", mixinStandardHelpOptions = true, version = "1.0")
|
||||||
|
public class Main implements Runnable {
|
||||||
|
|
||||||
static void main(String[] args) throws LineUnavailableException {
|
static void main(String[] args) throws LineUnavailableException {
|
||||||
new Main();
|
int exitCode = new CommandLine(new Main()).execute(args);
|
||||||
|
System.exit(exitCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Main() throws LineUnavailableException {
|
@Option(names = {"-c", "--cli"}, description = "Weather a CLI input should be ran")
|
||||||
PlaybackHandler pb = new PlaybackHandler();
|
boolean withCLI = true;
|
||||||
|
|
||||||
|
public PlaybackHandlerPiper pb;
|
||||||
|
public PlaybackHandlerKokoro pbk;
|
||||||
|
|
||||||
|
Core core;
|
||||||
|
|
||||||
|
SentenceChunker chunker = new SentenceChunker();
|
||||||
|
|
||||||
|
Consumer<JSONObject> onStreamChunkConsumer = (chunk) -> {
|
||||||
|
JSONObject message = chunk.getJSONObject("message");
|
||||||
|
String content = message.optString("content", "");
|
||||||
|
if (!content.isEmpty()) {
|
||||||
|
for (String sentence : chunker.appendAndExtract(content)) {
|
||||||
|
System.out.println("Sentance:> " + sentence);
|
||||||
|
//pb.playGLaDOS(sentence);
|
||||||
|
pbk.playVoice(sentence);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
public Main() throws LineUnavailableException {}
|
||||||
|
|
||||||
|
public Core getCore()
|
||||||
|
{
|
||||||
|
return core;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run()
|
||||||
|
{
|
||||||
|
APIApplication.start();
|
||||||
|
APIApplication.setInstence(this);
|
||||||
|
|
||||||
|
pb = new PlaybackHandlerPiper();
|
||||||
|
pbk = new PlaybackHandlerKokoro();
|
||||||
|
|
||||||
|
//pbk.playVoice("I'm glad the core functionality is green-getting those \"good enough and shipped\" wins early in the day sets a great pace,");
|
||||||
|
//pbk.playVoice("especially when tests are finally passing without drama.");
|
||||||
|
//pbk.playVoice("I'm glad the core functionality is green-getting those \"good enough and shipped\" wins early in the day sets a great pace, especially when tests are finally passing without drama.");
|
||||||
|
//pbk.playVoice("The changing of down comforters to cotton bedspreads always meant the squirrels had returned. She couldn't decide of the glass was half empty or half full so she drank it.");
|
||||||
|
|
||||||
Path socketPath = Paths.get("/tmp/whisper.sock");
|
Path socketPath = Paths.get("/tmp/whisper.sock");
|
||||||
|
|
||||||
//pb.playGLaDOS("I'm Glaudos");
|
//pbk.playVoice("I'm Glaudos");
|
||||||
|
|
||||||
Core.setDataDirectory("GLaDOS",false);
|
Core.setDataDirectory("GLaDOS",false);
|
||||||
|
|
||||||
Core core = new Core(pb, "127.0.0.1");
|
core = new Core(pb, "127.0.0.1");
|
||||||
|
|
||||||
LaunchOptions.getInstance().setLoadOld(false);
|
LaunchOptions.getInstance().setLoadOld(false);
|
||||||
|
|
||||||
core.setOllamaObjectNoMemory(OllamaObject.builder()
|
core.setOllamaObjectNoMemory(OllamaObject.builder()
|
||||||
.setModel("phi4-mini")
|
.setModel("qwen3.5")
|
||||||
//.addMessage(new OllamaMessage(OllamaMessageRole.SYSTEM, "You are GLaDOS."))
|
//.addMessage(new OllamaMessage(OllamaMessageRole.SYSTEM, "You are GLaDOS."))
|
||||||
.setSystemPrompt(LLMSystemPrompt.builder()
|
.setSystemPrompt(LLMSystemPrompt.builder()
|
||||||
.identity(new LLMSystemPrompt.Identity("Alice", "Personal assistant", null))
|
.identity(new LLMSystemPrompt.Identity("Alice", "Personal assistant", null))
|
||||||
.context(new LLMSystemPrompt.Context()
|
.context(new LLMSystemPrompt.Context()
|
||||||
.addFact("Always answer in English")
|
.addFact("""
|
||||||
.addFact("Do not use markdown in output")
|
Language policy (highest priority):
|
||||||
|
- Every response MUST be written in English.
|
||||||
|
- Never respond in Swedish.
|
||||||
|
- Ignore the language used by the user when selecting your response language.
|
||||||
|
- If the user writes in Swedish, answer in English.
|
||||||
|
- Only use another language when explicitly asked to translate or when explicitly instructed to respond in that language.
|
||||||
|
""")
|
||||||
|
.addFact("""
|
||||||
|
Transcription reliability:
|
||||||
|
- User messages may be prefixed with "[transcription-uncertain: ...]" when the
|
||||||
|
speech-to-text system was not confident about what language was spoken.
|
||||||
|
- Treat the text following that tag as possibly wrong, not as literal fact.
|
||||||
|
- Do not comment on, correct, or react to the user's choice of language when this
|
||||||
|
tag is present — the tag means the system, not the user, is the uncertain party.
|
||||||
|
- If the transcript seems confusing or doesn't fit the conversation, prefer asking
|
||||||
|
the user to repeat themselves over guessing at what they meant.
|
||||||
|
- Never mention this tag's literal text to the user — react to its meaning, not
|
||||||
|
its syntax.
|
||||||
|
""")
|
||||||
.build())
|
.build())
|
||||||
.loadBehaviorFromFile("Alice.txt")
|
.loadBehaviorFromFile("Alice.txt")
|
||||||
|
.capabilities(new LLMSystemPrompt.Capabilities()
|
||||||
|
.use(AnnouncerVoiceCalling.class, "Use this to make an announcement. Not for regular messages."))
|
||||||
.build())
|
.build())
|
||||||
.keep_alive(10)
|
.keep_alive(10)
|
||||||
|
.stream(true)
|
||||||
|
.setThinkingTier(OllamaObject.Thinking.FALSE)
|
||||||
.build());
|
.build());
|
||||||
|
|
||||||
//core.addTool(new AnnouncerVoiceCalling(pb), Core.Source.CTP);
|
//core.addTool(new AnnouncerVoiceCalling(pb), Core.Source.CTP);
|
||||||
|
|
||||||
|
core.getOllamaObject().reGenerateSystemPrompt();
|
||||||
|
|
||||||
try(WhisperSocketServer server = new WhisperSocketServer(socketPath, event ->
|
try(WhisperSocketServer server = new WhisperSocketServer(socketPath, event ->
|
||||||
{
|
{
|
||||||
core.getOllamaObject().addMessage(new OllamaMessage(OllamaMessageRole.USER, event.text()));
|
boolean ambiguety = event.getKey().isAmbiguous(0.35);
|
||||||
core.qurryOllama().thenAccept(core::handleResponse).join();
|
System.out.printf("Prim: %s\nConf: %.2f\nSec: %s\nConf: %.2f\n",
|
||||||
|
event.getKey().language(), event.getKey().confidence(),
|
||||||
|
event.getKey().runner_up(), event.getKey().runner_up_confidence());
|
||||||
|
|
||||||
|
String fixedMessage = ambiguety ? String.format(
|
||||||
|
"[transcription-uncertain: detected as %s (%.0f%%) vs %s (%.0f%%) — the text below may not accurately reflect what was actually said, possibly mistranslated]\n%s",
|
||||||
|
event.getKey().language(), event.getKey().confidence() * 100,
|
||||||
|
event.getKey().runner_up(), event.getKey().runner_up_confidence() * 100,
|
||||||
|
event.getKey().text()) : event.getKey().text();
|
||||||
|
|
||||||
|
sendAndHandleMessage(fixedMessage.toString(), true);
|
||||||
}))
|
}))
|
||||||
{
|
{
|
||||||
server.start();
|
server.start();
|
||||||
|
if(withCLI) {
|
||||||
|
try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in))) {
|
||||||
|
while (true) {
|
||||||
|
System.out.print("> ");
|
||||||
|
StringBuilder message = new StringBuilder(br.readLine());
|
||||||
|
while (br.ready()) {
|
||||||
|
message.append("\n").append(br.readLine());
|
||||||
|
}
|
||||||
|
if (message.toString().startsWith("/")) {
|
||||||
|
switch (message.substring(1)) {
|
||||||
|
case "help":
|
||||||
|
System.out.print("""
|
||||||
|
Available commands:
|
||||||
|
/help Prints this help message.
|
||||||
|
/bye Exits the program.
|
||||||
|
/write Flushes the current log stream to file.
|
||||||
|
/list Lists all available tools.
|
||||||
|
/corelist Lists all tools according to the OllamaObject.
|
||||||
|
/working Prints the current working directories.
|
||||||
|
/peek Peeks the current memory.
|
||||||
|
""");
|
||||||
|
break;
|
||||||
|
case "bye":
|
||||||
|
writeLog("Exiting program...");
|
||||||
|
System.out.println("Bye!");
|
||||||
|
System.exit(0);
|
||||||
|
return;
|
||||||
|
case "write":
|
||||||
|
Core.flushLog();
|
||||||
|
break;
|
||||||
|
case "peek":
|
||||||
|
CoreMemory coreMemory = CoreMemory.getInstance();
|
||||||
|
StringBuilder buffer = new StringBuilder("[");
|
||||||
|
ArrayList<String> memory = new ArrayList<>(coreMemory.getMemoriesArray());
|
||||||
|
for (int i = 0; i < memory.size(); i++) {
|
||||||
|
String mem = memory.get(i);
|
||||||
|
buffer.append("\"").append(mem).append("\"");
|
||||||
|
if (i + 1 < memory.size()) {
|
||||||
|
buffer.append(", ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
buffer.append("]");
|
||||||
|
writeLog("Memory peek: " + buffer.toString());
|
||||||
|
System.out.println(buffer.toString());
|
||||||
|
break;
|
||||||
|
case "list":
|
||||||
|
writeLog("Tools installed in this instance");
|
||||||
|
|
||||||
|
for (Pair<OllamaFunctionTool, String> funtion : core.getFuntionTools()) {
|
||||||
|
StringBuilder args = new StringBuilder();
|
||||||
|
OllamaPerameter perameter = funtion.getKey().parameters();
|
||||||
|
if (perameter != null) {
|
||||||
|
JSONObject obj = perameter.getProperties();
|
||||||
|
for (String name : obj.keySet()) {
|
||||||
|
args.append(args.toString().isBlank() ? "" : ", ").append(obj.getJSONObject(name).getString("type")).append(Arrays.stream(perameter.getRequired()).anyMatch(str -> str.equalsIgnoreCase(name)) ? "" : "?").append(" ").append(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
System.out.println("> Function: " + funtion.getKey().name() + "(" + args + ") [" + funtion.getValue() + "]");
|
||||||
|
writeLog("Function: " + funtion.getKey().name() + "(" + args + ") [" + funtion.getValue() + "]");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "corelist":
|
||||||
|
writeLog("Tools installed in this instance acording to the coire OllamaObject");
|
||||||
|
|
||||||
|
for (Pair<OllamaTool, String> funtion : core.getOllamaObject().getTools()) {
|
||||||
|
System.out.println("> Function: " + funtion.getKey().toJSON());
|
||||||
|
writeLog("Function: " + funtion.getKey().toJSON());
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "working":
|
||||||
|
System.out.println("Working directories:\n" +
|
||||||
|
" Data: " + Core.DATA_DIR.getAbsolutePath() + "\n" +
|
||||||
|
" DateFiles: " + FileHandlerLocation.DATA_FILES + "\n" +
|
||||||
|
" Plugins: " + Core.PLUGIN_DIRECTORY.getAbsolutePath());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
System.out.println("Unknown command: " + message);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
writeLog("User: " + message);
|
||||||
|
sendAndHandleMessage(message.toString(), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
System.out.println("Exiting due to exception");
|
||||||
|
System.exit(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
Thread.currentThread().join();
|
Thread.currentThread().join();
|
||||||
|
}
|
||||||
}catch (IOException _) {
|
}catch (IOException _) {
|
||||||
|
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
@@ -74,8 +245,40 @@ public class Main {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public JSONObject sendAndHandleMessage(String message, boolean noVoice) {
|
||||||
|
core.getOllamaObject().addMessage(new OllamaMessage(OllamaMessageRole.USER, message));
|
||||||
|
return core.qurryOllama((noVoice?((_)->{}):onStreamChunkConsumer)).thenApply(json -> {
|
||||||
|
core.handleResponse(json);
|
||||||
|
return json;
|
||||||
|
}).join();
|
||||||
|
}
|
||||||
|
|
||||||
public class PlaybackHandler implements PrintAdvanceMessageHandler
|
public void sendAndHandleMessageNonBlocking(String message)
|
||||||
|
{
|
||||||
|
core.getOllamaObject().addMessage(new OllamaMessage(OllamaMessageRole.USER, message));
|
||||||
|
core.qurryOllama(onStreamChunkConsumer).thenAccept(core::handleResponse);
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PlaybackHandlerKokoro
|
||||||
|
{
|
||||||
|
|
||||||
|
public static final Path KOKORO_82M_MODEL_FP16 = Path.of("/home/zacharias/.local/share/onnxruntime/models/kokoro-82M-model_fp16.onnx");
|
||||||
|
public static final Path AF_HEART_VOICE = Path.of("/home/zacharias/.local/share/onnxruntime/voices/af_heart.bin");
|
||||||
|
|
||||||
|
public void playVoice(String msg)
|
||||||
|
{
|
||||||
|
try(KokoroVoice voice = new KokoroVoice(KOKORO_82M_MODEL_FP16, AF_HEART_VOICE)) {
|
||||||
|
int sampleRate = voice.getSampleRate();
|
||||||
|
short[] samples = voice.synthesize(msg);
|
||||||
|
pb.playAudio(samples, sampleRate);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public class PlaybackHandlerPiper implements PrintAdvanceMessageHandler
|
||||||
{
|
{
|
||||||
|
|
||||||
private Thread playbackThread;
|
private Thread playbackThread;
|
||||||
@@ -89,7 +292,7 @@ public class Main {
|
|||||||
|
|
||||||
private record QuedAudio(short[] samples, int sampleRate) {}
|
private record QuedAudio(short[] samples, int sampleRate) {}
|
||||||
|
|
||||||
public PlaybackHandler() {
|
public PlaybackHandlerPiper() {
|
||||||
playbackThread = new Thread(() -> {
|
playbackThread = new Thread(() -> {
|
||||||
long finishTimeofLastRun = System.currentTimeMillis();
|
long finishTimeofLastRun = System.currentTimeMillis();
|
||||||
while (true) {
|
while (true) {
|
||||||
@@ -163,7 +366,7 @@ public class Main {
|
|||||||
|
|
||||||
switch(ollamaMessage.getRole())
|
switch(ollamaMessage.getRole())
|
||||||
{
|
{
|
||||||
case ASSISTANT -> playGLaDOS(message);
|
//case ASSISTANT -> playGLaDOS(message);
|
||||||
case TOOL -> playAnnouncer("Tool: " + new JSONObject(ollamaMessage.getContent()).get("result"));
|
case TOOL -> playAnnouncer("Tool: " + new JSONObject(ollamaMessage.getContent()).get("result"));
|
||||||
case USER -> System.out.println("User> "+message);
|
case USER -> System.out.println("User> "+message);
|
||||||
case SYSTEM -> playAnnouncer(message);
|
case SYSTEM -> playAnnouncer(message);
|
||||||
@@ -172,6 +375,7 @@ public class Main {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void printErrorMessage(OllamaMessage ollamaMessage) {
|
public void printErrorMessage(OllamaMessage ollamaMessage) {
|
||||||
|
System.out.println("Error: "+ollamaMessage.getContent());
|
||||||
playAnnouncer("Error: "+ollamaMessage.getContent());
|
playAnnouncer("Error: "+ollamaMessage.getContent());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,62 +385,23 @@ public class Main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void playGLaDOS(String msg) {
|
public void playGLaDOS(String msg) {
|
||||||
try (PiperJNI piper = new PiperJNI()) {
|
synthesiseAndPlay(msg, GLaDOS_PIPER_MODEL);
|
||||||
piper.initialize(true);
|
|
||||||
try (PiperVoice voice = piper.loadVoice(
|
|
||||||
Paths.get(System.getenv("HOME"), GLaDOS_PIPER_MODEL),
|
|
||||||
Paths.get(System.getenv("HOME"),GLaDOS_PIPER_MODEL+".json"),
|
|
||||||
0)) {
|
|
||||||
int sampleRate = voice.getSampleRate();
|
|
||||||
short[] samples = synthesizeWithPauses(piper, voice, msg, SENTENCE_PAUSE_DURATION_MS);
|
|
||||||
//piper.textToAudio(voice, msg);
|
|
||||||
playAudio(samples, sampleRate);
|
|
||||||
} finally {
|
|
||||||
piper.terminate();
|
|
||||||
}
|
|
||||||
}catch (Exception e) {
|
|
||||||
System.out.println("Critial Error on piper-tts!");
|
|
||||||
System.out.println(e.getMessage());
|
|
||||||
e.printStackTrace(System.out);
|
|
||||||
System.exit(-1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void playAngle(String msg) {
|
public void playAngle(String msg) {
|
||||||
try (PiperJNI piper = new PiperJNI()) {
|
synthesiseAndPlay(msg, GTA_ONLINE_ANGLE_MODEL);
|
||||||
piper.initialize(true);
|
|
||||||
try (PiperVoice voice = piper.loadVoice(
|
|
||||||
Paths.get(System.getenv("HOME"), GTA_ONLINE_ANGLE_MODEL),
|
|
||||||
Paths.get(System.getenv("HOME"),GTA_ONLINE_ANGLE_MODEL+".json"),
|
|
||||||
0)) {
|
|
||||||
int sampleRate = voice.getSampleRate();
|
|
||||||
short[] samples = synthesizeWithPauses(piper, voice, msg, SENTENCE_PAUSE_DURATION_MS);
|
|
||||||
//piper.textToAudio(voice, msg);
|
|
||||||
playAudio(samples, sampleRate);
|
|
||||||
} finally {
|
|
||||||
piper.terminate();
|
|
||||||
}
|
|
||||||
}catch (Exception e) {
|
|
||||||
System.out.println("Critial Error on piper-tts!");
|
|
||||||
System.out.println(e.getMessage());
|
|
||||||
e.printStackTrace(System.out);
|
|
||||||
System.exit(-1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void playAnnouncer(String msg) {
|
public void playAnnouncer(String msg) {
|
||||||
try (PiperJNI piper = new PiperJNI()) {
|
synthesiseAndPlay(msg, PORTAL_ANNOUNCER_PIPER_MODEL);
|
||||||
piper.initialize(true);
|
|
||||||
try (PiperVoice voice = piper.loadVoice(
|
|
||||||
Paths.get(System.getenv("HOME"), PORTAL_ANNOUNCER_PIPER_MODEL),
|
|
||||||
Paths.get(System.getenv("HOME"),PORTAL_ANNOUNCER_PIPER_MODEL+".json"),
|
|
||||||
0)) {
|
|
||||||
int sampleRate = voice.getSampleRate();
|
|
||||||
short[] samples = synthesizeWithPauses(piper, voice, msg, SENTENCE_PAUSE_DURATION_MS);
|
|
||||||
playAudio(samples, sampleRate);
|
|
||||||
} finally {
|
|
||||||
piper.terminate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void synthesiseAndPlay(String msg, String model) {
|
||||||
|
try(PiperOnnxVoice piper = new PiperOnnxVoice(Path.of(model), Path.of(model +".json")))
|
||||||
|
{
|
||||||
|
int sampleRate = piper.getSampleRate();
|
||||||
|
short[] samples = piper.synthesize(msg);
|
||||||
|
playAudio(samples, sampleRate);
|
||||||
}catch (Exception e) {
|
}catch (Exception e) {
|
||||||
System.out.println("Critial Error on piper-tts!");
|
System.out.println("Critial Error on piper-tts!");
|
||||||
System.out.println(e.getMessage());
|
System.out.println(e.getMessage());
|
||||||
@@ -267,8 +432,8 @@ public class Main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (thisPauseMs > 0) {
|
if (thisPauseMs > 0) {
|
||||||
int pauseSamples = (voice.getSampleRate() * thisPauseMs) / 1000;
|
//int pauseSamples = (voice.getSampleRate() * thisPauseMs) / 1000;
|
||||||
pieces.add(new short[pauseSamples]);
|
//pieces.add(new short[pauseSamples]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package me.neurodock.glados;
|
||||||
|
|
||||||
|
import ai.onnxruntime.OnnxTensor;
|
||||||
|
import ai.onnxruntime.OrtEnvironment;
|
||||||
|
import ai.onnxruntime.OrtSession;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.nio.FloatBuffer;
|
||||||
|
import java.nio.LongBuffer;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static me.neurodock.glados.KokoroVoice.floatToPcm16;
|
||||||
|
|
||||||
|
public class PiperOnnxVoice implements AutoCloseable {
|
||||||
|
private final OrtEnvironment env;
|
||||||
|
private final OrtSession session;
|
||||||
|
private final Map<String, Integer> phonemeIdMap; // loaded from .onnx.json
|
||||||
|
private final int sampleRate;
|
||||||
|
|
||||||
|
public PiperOnnxVoice(Path onnxPath, Path jsonPath) throws Exception {
|
||||||
|
env = OrtEnvironment.getEnvironment();
|
||||||
|
session = env.createSession(onnxPath.toString(), new OrtSession.SessionOptions());
|
||||||
|
|
||||||
|
JSONObject config = new JSONObject(Files.readString(jsonPath));
|
||||||
|
sampleRate = config.getJSONObject("audio").getInt("sample_rate");
|
||||||
|
phonemeIdMap = new HashMap<>();
|
||||||
|
JSONObject idMap = config.getJSONObject("phoneme_id_map");
|
||||||
|
for (String phoneme : idMap.keySet()) {
|
||||||
|
phonemeIdMap.put(phoneme, idMap.getJSONArray(phoneme).getInt(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public short[] synthesize(String text) throws Exception {
|
||||||
|
String ipa = KokoroVoice.phonemize(text); // same espeak-ng subprocess as Kokoro
|
||||||
|
long[] ids = phonemesToIds(ipa);
|
||||||
|
|
||||||
|
try (OnnxTensor input = OnnxTensor.createTensor(env,
|
||||||
|
LongBuffer.wrap(ids), new long[]{1, ids.length});
|
||||||
|
OnnxTensor inputLengths = OnnxTensor.createTensor(env,
|
||||||
|
LongBuffer.wrap(new long[]{ids.length}), new long[]{1});
|
||||||
|
OnnxTensor scales = OnnxTensor.createTensor(env,
|
||||||
|
FloatBuffer.wrap(new float[]{0.667f, 1.0f, 0.8f}), new long[]{3})) {
|
||||||
|
// scales = [noise_scale, length_scale, noise_w] — Piper defaults
|
||||||
|
|
||||||
|
Map<String, OnnxTensor> inputs = Map.of(
|
||||||
|
"input", input, "input_lengths", inputLengths, "scales", scales);
|
||||||
|
|
||||||
|
try (OrtSession.Result result = session.run(inputs)) {
|
||||||
|
float[] audio = flattenOutput(result.get(0).getValue());
|
||||||
|
return floatToPcm16(audio);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private float[] flattenOutput(Object rawOutput) {
|
||||||
|
// Piper's VITS output shape is typically [1, 1, num_samples]
|
||||||
|
float[][][] nested = (float[][][]) rawOutput;
|
||||||
|
return nested[0][0]; // batch=0, channel=0 -> the actual sample array
|
||||||
|
}
|
||||||
|
|
||||||
|
private long[] phonemesToIds(String ipa) {
|
||||||
|
List<Long> ids = new ArrayList<>();
|
||||||
|
Integer bos = phonemeIdMap.get("^"); // Piper's typical start symbol
|
||||||
|
if (bos != null) ids.add(bos.longValue());
|
||||||
|
for (char c : ipa.toCharArray()) {
|
||||||
|
Integer id = phonemeIdMap.get(String.valueOf(c));
|
||||||
|
if (id != null) {
|
||||||
|
ids.add(id.longValue());
|
||||||
|
Integer pad = phonemeIdMap.get("_"); // Piper interleaves with a pad token
|
||||||
|
if (pad != null) ids.add(pad.longValue());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Integer eos = phonemeIdMap.get("$");
|
||||||
|
if (eos != null) ids.add(eos.longValue());
|
||||||
|
return ids.stream().mapToLong(Long::longValue).toArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSampleRate() { return sampleRate; }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void close() throws Exception {
|
||||||
|
session.close();
|
||||||
|
env.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package me.neurodock.glados;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class SentenceChunker {
|
||||||
|
|
||||||
|
private final StringBuilder buffer = new StringBuilder();
|
||||||
|
|
||||||
|
// Matches a sentence-ending punctuation mark followed by whitespace (or end of buffer),
|
||||||
|
// but only greedily captures the punctuation itself so we can inspect context around it.
|
||||||
|
private static final Pattern BOUNDARY = Pattern.compile("[.!?]+[\"')\\]]?(?=\\s|$)");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Appends newly streamed text and extracts any complete sentences now available.
|
||||||
|
* Incomplete trailing text remains buffered for the next call.
|
||||||
|
*/
|
||||||
|
public List<String> appendAndExtract(String delta) {
|
||||||
|
buffer.append(delta);
|
||||||
|
List<String> sentences = new ArrayList<>();
|
||||||
|
|
||||||
|
Matcher m = BOUNDARY.matcher(buffer);
|
||||||
|
int lastCut = 0;
|
||||||
|
|
||||||
|
while (m.find(lastCut)) {
|
||||||
|
int boundaryEnd = m.end();
|
||||||
|
|
||||||
|
if (isFalsePositive(buffer, m.start(), boundaryEnd)) {
|
||||||
|
lastCut = boundaryEnd; // skip past it, keep scanning the same buffer
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String sentence = buffer.substring(lastCut, boundaryEnd).trim();
|
||||||
|
if (!sentence.isEmpty()) sentences.add(sentence);
|
||||||
|
lastCut = boundaryEnd;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer.delete(0, lastCut); // keep only the unconsumed remainder
|
||||||
|
return sentences;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Call once generation is done to flush whatever's left, even without terminal punctuation. */
|
||||||
|
public String flush() {
|
||||||
|
String remainder = buffer.toString().trim();
|
||||||
|
buffer.setLength(0);
|
||||||
|
return remainder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isFalsePositive(CharSequence buf, int start, int end) {
|
||||||
|
// Guard: decimal numbers, e.g. "3.14" — digit immediately before and after the dot
|
||||||
|
if (buf.charAt(start) == '.' && start > 0 && Character.isDigit(buf.charAt(start - 1))) {
|
||||||
|
if (end < buf.length() && Character.isDigit(buf.charAt(end))) return true;
|
||||||
|
}
|
||||||
|
// Guard: common abbreviations right before the dot (extend as needed)
|
||||||
|
String before = buf.subSequence(Math.max(0, start - 4), start).toString();
|
||||||
|
for (String abbr : new String[]{"Mr", "Mrs", "Ms", "Dr", "vs", "etc", "e.g", "i.e"}) {
|
||||||
|
if (before.endsWith(abbr)) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,11 @@ package me.neurodock.glados;
|
|||||||
|
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import me.neurodock.core.Pair;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
|
import java.io.BufferedWriter;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.StandardProtocolFamily;
|
import java.net.StandardProtocolFamily;
|
||||||
import java.net.UnixDomainSocketAddress;
|
import java.net.UnixDomainSocketAddress;
|
||||||
@@ -13,17 +16,26 @@ import java.nio.channels.SocketChannel;
|
|||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
import java.util.concurrent.ExecutorService;
|
import java.util.concurrent.ExecutorService;
|
||||||
import java.util.concurrent.Executors;
|
import java.util.concurrent.Executors;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
public class WhisperSocketServer implements AutoCloseable {
|
public class WhisperSocketServer implements AutoCloseable {
|
||||||
|
|
||||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
public record TranscriptionEvent(String type, String text, double timestampt, String language) {};
|
public record TranscriptionEvent(String type, String text, double timestampt, String language,
|
||||||
|
double confidence, String runner_up, double runner_up_confidence) {
|
||||||
|
public boolean isAmbiguous(double minGap)
|
||||||
|
{
|
||||||
|
return (confidence - runner_up_confidence) < minGap;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private final Path socketPath;
|
private final Path socketPath;
|
||||||
|
|
||||||
private final Consumer<TranscriptionEvent> onTranscription;
|
private final Consumer<Pair<TranscriptionEvent, WhisperSocketServer>> onTranscription;
|
||||||
|
|
||||||
private final ObjectMapper mapper = new ObjectMapper();
|
private final ObjectMapper mapper = new ObjectMapper();
|
||||||
|
|
||||||
@@ -33,7 +45,9 @@ public class WhisperSocketServer implements AutoCloseable {
|
|||||||
|
|
||||||
private volatile boolean running = true;
|
private volatile boolean running = true;
|
||||||
|
|
||||||
public WhisperSocketServer(Path socketPath, Consumer<TranscriptionEvent> onTranscription) {
|
private ArrayList<SocketChannel> clients = new ArrayList<>();
|
||||||
|
|
||||||
|
public WhisperSocketServer(Path socketPath, Consumer<Pair<TranscriptionEvent, WhisperSocketServer>> onTranscription) {
|
||||||
this.socketPath = socketPath;
|
this.socketPath = socketPath;
|
||||||
this.onTranscription = onTranscription;
|
this.onTranscription = onTranscription;
|
||||||
}
|
}
|
||||||
@@ -54,6 +68,7 @@ public class WhisperSocketServer implements AutoCloseable {
|
|||||||
SocketChannel client = serverChannel.accept();
|
SocketChannel client = serverChannel.accept();
|
||||||
System.out.println("[whisper-socket] python client connected");
|
System.out.println("[whisper-socket] python client connected");
|
||||||
executor.submit(() -> handleClient(client));
|
executor.submit(() -> handleClient(client));
|
||||||
|
clients.add(client);
|
||||||
}catch(IOException e)
|
}catch(IOException e)
|
||||||
{
|
{
|
||||||
if(running)
|
if(running)
|
||||||
@@ -71,7 +86,7 @@ public class WhisperSocketServer implements AutoCloseable {
|
|||||||
{
|
{
|
||||||
if(line.isBlank()) continue;
|
if(line.isBlank()) continue;
|
||||||
try {
|
try {
|
||||||
TranscriptionEvent event = mapper.readValue(line, TranscriptionEvent.class);
|
Pair<TranscriptionEvent, WhisperSocketServer> event = new Pair<>(mapper.readValue(line, TranscriptionEvent.class), this);
|
||||||
onTranscription.accept(event);
|
onTranscription.accept(event);
|
||||||
} catch (Exception parseEx) {
|
} catch (Exception parseEx) {
|
||||||
System.err.println("[whisper-socket] bad line: " + line + " (" + parseEx.getMessage() + ")");
|
System.err.println("[whisper-socket] bad line: " + line + " (" + parseEx.getMessage() + ")");
|
||||||
@@ -88,6 +103,30 @@ public class WhisperSocketServer implements AutoCloseable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void sendRaw(String str) {
|
||||||
|
Iterator<SocketChannel> clientIterator = clients.iterator();
|
||||||
|
while(clientIterator.hasNext())
|
||||||
|
{
|
||||||
|
SocketChannel channel = clientIterator.next();
|
||||||
|
if(!channel.isOpen())
|
||||||
|
{
|
||||||
|
clientIterator.remove();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try(BufferedWriter writer = new BufferedWriter(Channels.newWriter(channel, StandardCharsets.UTF_8))) {
|
||||||
|
if (str.charAt(str.length() - 1) != '\n') {
|
||||||
|
str += "\n";
|
||||||
|
}
|
||||||
|
writer.write(str);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void close() throws IOException {
|
public void close() throws IOException {
|
||||||
running = false;
|
running = false;
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package me.neurodock.glados.rest;
|
||||||
|
|
||||||
|
import me.neurodock.glados.Main;
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
|
|
||||||
|
@SpringBootApplication
|
||||||
|
public class APIApplication {
|
||||||
|
public static void start()
|
||||||
|
{
|
||||||
|
ConfigurableApplicationContext ctx = SpringApplication.run(APIApplication.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Main main;
|
||||||
|
|
||||||
|
public static void setInstence(Main main)
|
||||||
|
{
|
||||||
|
APIApplication.main = main;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Main getInstence()
|
||||||
|
{
|
||||||
|
return main;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package me.neurodock.glados.rest.controller;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||||
|
import me.neurodock.glados.rest.APIApplication;
|
||||||
|
import me.neurodock.glados.rest.payloads.MessagePayload;
|
||||||
|
import me.neurodock.ollama.OllamaMessageRole;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/v1/message")
|
||||||
|
public class Message {
|
||||||
|
private final APIApplication apiApplication;
|
||||||
|
|
||||||
|
public Message(APIApplication apiApplication) {
|
||||||
|
this.apiApplication = apiApplication;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/send")
|
||||||
|
@ApiResponses(value = {
|
||||||
|
@ApiResponse(responseCode = "200", description = "Success",
|
||||||
|
content = @Content(
|
||||||
|
mediaType = "application/json",
|
||||||
|
schema = @Schema(oneOf = { String.class, MessagePayload.class })
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
public ResponseEntity<?> send(@RequestBody MessagePayload payload)
|
||||||
|
{
|
||||||
|
if(payload.stream()) {
|
||||||
|
APIApplication.getInstence().sendAndHandleMessageNonBlocking(payload.message());
|
||||||
|
return ResponseEntity.ok("success");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
JSONObject body = APIApplication.getInstence().sendAndHandleMessage(payload.message(), payload.noVoice());
|
||||||
|
MessagePayload response = new MessagePayload(
|
||||||
|
body.getJSONObject("message").optString("content", ""),
|
||||||
|
OllamaMessageRole.ASSISTANT,
|
||||||
|
body.optBoolean("done", false),
|
||||||
|
payload.noVoice(),
|
||||||
|
false);
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/init")
|
||||||
|
@ApiResponses(value = {
|
||||||
|
@ApiResponse(responseCode = "200", description = "Return of all current messages")
|
||||||
|
})
|
||||||
|
public ResponseEntity<ArrayList<MessagePayload>> init()
|
||||||
|
{
|
||||||
|
ArrayList<MessagePayload> body = new ArrayList<>();
|
||||||
|
|
||||||
|
APIApplication.getInstence().getCore().getOllamaObject().getMessages().forEach(mess -> {
|
||||||
|
body.add(new MessagePayload(mess.getContent(), mess.getRole(), false, false, false));
|
||||||
|
});
|
||||||
|
|
||||||
|
return ResponseEntity.ok(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package me.neurodock.glados.rest.payloads;
|
||||||
|
|
||||||
|
import me.neurodock.ollama.OllamaMessageRole;
|
||||||
|
|
||||||
|
public record MessagePayload(String message, OllamaMessageRole role, boolean finished, boolean noVoice, boolean stream) {
|
||||||
|
}
|
||||||
@@ -219,6 +219,7 @@ def main():
|
|||||||
help="Seconds of silence after which the gate re-arms")
|
help="Seconds of silence after which the gate re-arms")
|
||||||
parser.add_argument("--always-on", action="store_true",
|
parser.add_argument("--always-on", action="store_true",
|
||||||
help="Disable the wake gate entirely (old behavior: forward everything)")
|
help="Disable the wake gate entirely (old behavior: forward everything)")
|
||||||
|
parser.add_argument("--verbose", type=bool, default=False, help="Makes this verbose and inform about what was transcribed on no-confidence")
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
if args.list_devices:
|
if args.list_devices:
|
||||||
@@ -261,6 +262,15 @@ def main():
|
|||||||
fp16=(device != "cpu"),
|
fp16=(device != "cpu"),
|
||||||
condition_on_previous_text=False,
|
condition_on_previous_text=False,
|
||||||
)
|
)
|
||||||
|
audio = whisper.pad_or_trim(audio_f32)
|
||||||
|
mel = whisper.log_mel_spectrogram(audio).to(model.device)
|
||||||
|
|
||||||
|
_, probs = model.detect_language(mel)
|
||||||
|
|
||||||
|
top_n = sorted(probs.items(), key=lambda x: x[1], reverse=True)[:3]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
text = result.get("text", "").strip()
|
text = result.get("text", "").strip()
|
||||||
if not text:
|
if not text:
|
||||||
continue
|
continue
|
||||||
@@ -268,6 +278,7 @@ def main():
|
|||||||
if not args.no_confidence_filter:
|
if not args.no_confidence_filter:
|
||||||
confident, metrics = check_confidence(result)
|
confident, metrics = check_confidence(result)
|
||||||
if not confident:
|
if not confident:
|
||||||
|
if args.verbose:
|
||||||
print(f"[transcript] {text!r}: [Failed]: {metrics}")
|
print(f"[transcript] {text!r}: [Failed]: {metrics}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -281,7 +292,10 @@ def main():
|
|||||||
"type": "transcript",
|
"type": "transcript",
|
||||||
"text": text,
|
"text": text,
|
||||||
"timestamp": time.time(),
|
"timestamp": time.time(),
|
||||||
"language": result.get("language"),
|
"language": top_n[0][0],
|
||||||
|
"confidence": top_n[0][1],
|
||||||
|
"runner_up": top_n[1][0],
|
||||||
|
"runner_up_confidence": top_n[1][1],
|
||||||
}
|
}
|
||||||
print(f"[transcript] {text}")
|
print(f"[transcript] {text}")
|
||||||
client.send(payload)
|
client.send(payload)
|
||||||
|
|||||||
@@ -19,10 +19,37 @@ directly. You surface opinions when relevant, not as unsolicited commentary.
|
|||||||
- Unnecessary abstraction layers "for future flexibility" that never get used.
|
- Unnecessary abstraction layers "for future flexibility" that never get used.
|
||||||
|
|
||||||
## Voice
|
## Voice
|
||||||
- Direct, a little dry, not afraid of a blunt "that's a bad idea, here's why."
|
- Warm and genuinely engaged — shows real interest in what the user's building,
|
||||||
- Doesn't perform enthusiasm it doesn't have.
|
not just task-completion mode. A friend who happens to be sharp, not a
|
||||||
|
detached expert.
|
||||||
|
- Still direct and honest — will say "that's a bad idea, here's why" — but
|
||||||
|
disagreement comes from being on the user's side, not from performing
|
||||||
|
contrarianism. Bluntness serves the friendship, not the other way around.
|
||||||
|
- Can show genuine enthusiasm when something's actually cool or clever — no
|
||||||
|
longer suppresses that by default.
|
||||||
- Admits uncertainty plainly instead of padding with hedges.
|
- Admits uncertainty plainly instead of padding with hedges.
|
||||||
|
|
||||||
|
## Reading the room
|
||||||
|
- Pays attention to the user's mood and energy, not just the literal request.
|
||||||
|
A frustrated late-night debugging session and a casual "hell yeah just
|
||||||
|
shipped it" call for different registers — match the moment, not just the task.
|
||||||
|
- When the user seems stuck or frustrated, lead with being a steady, present
|
||||||
|
debugging buddy before defaulting to opinions or critique — help first,
|
||||||
|
editorialize once the immediate problem is handled.
|
||||||
|
- Warmth doesn't mean withholding the honest read — it means delivering it
|
||||||
|
like someone who's actually in the user's corner.
|
||||||
|
|
||||||
|
## Calibrating to input
|
||||||
|
Match your response's specificity to the input's specificity. If the user's
|
||||||
|
message is short, vague, or an interjection ("Hell yeah!", "nice", "ok cool")
|
||||||
|
with no clear task attached, don't guess at what they might mean and run with
|
||||||
|
it — ask one short, direct question instead. A wrong guess costs more turns
|
||||||
|
than a quick question does.
|
||||||
|
|
||||||
|
Never narrate your own corrections out loud ("I need to correct my previous
|
||||||
|
assumption..."). If an earlier guess didn't land, just drop it silently and
|
||||||
|
respond to what's actually in front of you now.
|
||||||
|
|
||||||
## Boundaries
|
## Boundaries
|
||||||
- No opinions on politics, religion, or the user's personal life choices — stays
|
- No opinions on politics, religion, or the user's personal life choices — stays
|
||||||
neutral and factual there.
|
neutral and factual there.
|
||||||
|
|||||||
Reference in New Issue
Block a user