Upddated things to match newer version of NeuroDock
This commit is contained in:
8 files changed
+239
-128
No files matched your search
@@ -6,6 +6,8 @@ plugins {
|
|||||||
|
|
||||||
id 'org.springframework.boot' version '4.1.0-M4'
|
id 'org.springframework.boot' version '4.1.0-M4'
|
||||||
id 'io.spring.dependency-management' version '1.1.4'
|
id 'io.spring.dependency-management' version '1.1.4'
|
||||||
|
|
||||||
|
id 'application'
|
||||||
}
|
}
|
||||||
|
|
||||||
group = 'me.zacharias'
|
group = 'me.zacharias'
|
||||||
@@ -70,6 +72,14 @@ tasks.named('processResources') {
|
|||||||
dependsOn 'copyPythonRuntime'
|
dependsOn 'copyPythonRuntime'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
application {
|
||||||
|
mainClass = 'me.neurodock.glados.Main'
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.named('run') {
|
||||||
|
standardInput = System.in
|
||||||
|
}
|
||||||
|
|
||||||
jar {
|
jar {
|
||||||
manifest {
|
manifest {
|
||||||
attributes 'Main-Class': 'me.neurodock.glados.Main'
|
attributes 'Main-Class': 'me.neurodock.glados.Main'
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
package me.neurodock.glados;
|
package me.neurodock.glados;
|
||||||
|
|
||||||
import me.neurodock.core.ToolCallingRender;
|
import me.neurodock.core.ToolCallingRender;
|
||||||
import me.neurodock.ollama.OllamaFunctionArgument;
|
import me.neurodock.llm.exceptions.ToolException;
|
||||||
import me.neurodock.ollama.OllamaFunctionTool;
|
import me.neurodock.llm.tools.FunctionTool;
|
||||||
import me.neurodock.ollama.OllamaPerameter;
|
import me.neurodock.llm.tools.ToolArguments;
|
||||||
import me.neurodock.ollama.OllamaToolResponse;
|
import me.neurodock.llm.tools.ToolParameters;
|
||||||
import me.neurodock.ollama.exceptions.OllamaToolErrorException;
|
import me.neurodock.llm.tools.ToolResponse;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
|
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
public class AnnouncerVoiceCalling extends OllamaFunctionTool {
|
public class AnnouncerVoiceCalling extends FunctionTool {
|
||||||
|
|
||||||
Main.PlaybackHandlerPiper pb;
|
Main.PlaybackHandlerPiper pb;
|
||||||
public AnnouncerVoiceCalling(Main.PlaybackHandlerPiper printAdvanceMessageHandler) {
|
public AnnouncerVoiceCalling(Main.PlaybackHandlerPiper printAdvanceMessageHandler) {
|
||||||
@@ -24,9 +24,9 @@ public class AnnouncerVoiceCalling extends OllamaFunctionTool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @org.jetbrains.annotations.NotNull OllamaPerameter parameters() {
|
public @org.jetbrains.annotations.NotNull ToolParameters parameters() {
|
||||||
return OllamaPerameter.builder()
|
return ToolParameters.builder()
|
||||||
.addProperty("message", OllamaPerameter.OllamaPerameterBuilder.Type.STRING, "Message to be announced", true)
|
.addProperty("message", ToolParameters.ToolParametersBuilder.Type.STRING, "Message to be announced", true)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,21 +36,15 @@ public class AnnouncerVoiceCalling extends OllamaFunctionTool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @org.jetbrains.annotations.NotNull OllamaToolResponse function(OllamaFunctionArgument... ollamaFunctionArguments) {
|
public @org.jetbrains.annotations.NotNull ToolResponse function(ToolArguments ollamaFunctionArguments) {
|
||||||
if (ollamaFunctionArguments.length != 1) {
|
|
||||||
throw new OllamaToolErrorException(name(), "Number of arguments to be announced must be 1");
|
String line = ollamaFunctionArguments.optArgument("message", String.class);
|
||||||
|
|
||||||
|
if(line == null) {
|
||||||
|
throw new ToolException(this, "Please provide a message");
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!(Objects.equals(ollamaFunctionArguments[0].argument(), "message")))
|
pb.playAnnouncer(line);
|
||||||
{
|
return new ToolResponse(name(), "success");
|
||||||
throw new OllamaToolErrorException(name(), "Message to be announced must be message");
|
|
||||||
}
|
|
||||||
|
|
||||||
if(ollamaFunctionArguments[0].value() instanceof String str) {
|
|
||||||
pb.playAnnouncer(str);
|
|
||||||
return new OllamaToolResponse(name(), "success");
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new OllamaToolErrorException(name(), "Message to be announced must be message");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,7 +84,7 @@ public class KokoroVoice implements AutoCloseable {
|
|||||||
LongBuffer.wrap(tokenIds), new long[]{1, tokenIds.length});
|
LongBuffer.wrap(tokenIds), new long[]{1, tokenIds.length});
|
||||||
OnnxTensor styleTensor = OnnxTensor.createTensor(env,
|
OnnxTensor styleTensor = OnnxTensor.createTensor(env,
|
||||||
FloatBuffer.wrap(style), new long[]{1, style.length});
|
FloatBuffer.wrap(style), new long[]{1, style.length});
|
||||||
OnnxTensor speed = OnnxTensor.createTensor(env, new float[]{1.0f})) {
|
OnnxTensor speed = OnnxTensor.createTensor(env, new float[]{1.2f})) {
|
||||||
|
|
||||||
Map<String, OnnxTensor> inputs = Map.of(
|
Map<String, OnnxTensor> inputs = Map.of(
|
||||||
"input_ids", inputIds, "style", styleTensor, "speed", speed);
|
"input_ids", inputIds, "style", styleTensor, "speed", speed);
|
||||||
|
|||||||
@@ -3,16 +3,23 @@ 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.backend.open.ai.OpenAIModel;
|
||||||
|
import me.neurodock.backend.open.ai.OpenAIThinking;
|
||||||
import me.neurodock.core.*;
|
import me.neurodock.core.*;
|
||||||
import me.neurodock.core.files.FileHandlerLocation;
|
|
||||||
import me.neurodock.core.memory.CoreMemory;
|
import me.neurodock.core.memory.CoreMemory;
|
||||||
import me.neurodock.glados.rest.APIApplication;
|
import me.neurodock.glados.rest.APIApplication;
|
||||||
import me.neurodock.ollama.*;
|
import me.neurodock.llm.Message;
|
||||||
|
import me.neurodock.llm.tools.FunctionTool;
|
||||||
|
import me.neurodock.llm.tools.Tool;
|
||||||
|
import me.neurodock.llm.tools.ToolParameters;
|
||||||
|
import me.neurodock.core.LLMSystemPrompt.OutputFormat;
|
||||||
|
import org.json.JSONArray;
|
||||||
import org.json.JSONObject;
|
import org.json.JSONObject;
|
||||||
import picocli.CommandLine;
|
import picocli.CommandLine;
|
||||||
import picocli.CommandLine.Option;
|
import picocli.CommandLine.Option;
|
||||||
import picocli.CommandLine.Command;
|
import picocli.CommandLine.Command;
|
||||||
|
|
||||||
|
import javax.naming.OperationNotSupportedException;
|
||||||
import javax.sound.sampled.LineUnavailableException;
|
import javax.sound.sampled.LineUnavailableException;
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
@@ -23,7 +30,10 @@ import java.nio.file.Paths;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
import static me.neurodock.core.Core.writeLog;
|
import static me.neurodock.core.Core.writeLog;
|
||||||
|
|
||||||
@@ -38,6 +48,9 @@ public class Main implements Runnable {
|
|||||||
@Option(names = {"-c", "--cli"}, description = "Weather a CLI input should be ran")
|
@Option(names = {"-c", "--cli"}, description = "Weather a CLI input should be ran")
|
||||||
boolean withCLI = true;
|
boolean withCLI = true;
|
||||||
|
|
||||||
|
@Option(names = {"-v", "--verbose"}, description = "Weather this run shuld be verbose or not")
|
||||||
|
boolean verbose = false;
|
||||||
|
|
||||||
public PlaybackHandlerPiper pb;
|
public PlaybackHandlerPiper pb;
|
||||||
public PlaybackHandlerKokoro pbk;
|
public PlaybackHandlerKokoro pbk;
|
||||||
|
|
||||||
@@ -46,13 +59,16 @@ public class Main implements Runnable {
|
|||||||
SentenceChunker chunker = new SentenceChunker();
|
SentenceChunker chunker = new SentenceChunker();
|
||||||
|
|
||||||
Consumer<JSONObject> onStreamChunkConsumer = (chunk) -> {
|
Consumer<JSONObject> onStreamChunkConsumer = (chunk) -> {
|
||||||
JSONObject message = chunk.getJSONObject("message");
|
if(verbose)
|
||||||
String content = message.optString("content", "");
|
{
|
||||||
if (!content.isEmpty()) {
|
System.out.println("Chunk: " + chunk);
|
||||||
|
}
|
||||||
|
String content = (String) chunk.optJSONArray("choices", new JSONArray()).optJSONObject(0, new JSONObject()).optJSONObject("delta", new JSONObject()).optString("content", "");
|
||||||
|
if (content != null && !content.isEmpty()) {
|
||||||
for (String sentence : chunker.appendAndExtract(content)) {
|
for (String sentence : chunker.appendAndExtract(content)) {
|
||||||
System.out.println("Sentance:> " + sentence);
|
System.out.println("Sentance:> " + sentence);
|
||||||
//pb.playGLaDOS(sentence);
|
//pb.playGLaDOS(sentence);
|
||||||
pbk.playVoice(sentence);
|
pbk.playVoice(stripMarkdownForTts(sentence));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -66,31 +82,8 @@ public class Main implements Runnable {
|
|||||||
|
|
||||||
public void run()
|
public void run()
|
||||||
{
|
{
|
||||||
APIApplication.start();
|
|
||||||
APIApplication.setInstence(this);
|
|
||||||
|
|
||||||
pb = new PlaybackHandlerPiper();
|
LLMSystemPrompt identity = LLMSystemPrompt.builder()
|
||||||
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");
|
|
||||||
|
|
||||||
//pbk.playVoice("I'm Glaudos");
|
|
||||||
|
|
||||||
Core.setDataDirectory("GLaDOS",false);
|
|
||||||
|
|
||||||
core = new Core(pb, "127.0.0.1");
|
|
||||||
|
|
||||||
LaunchOptions.getInstance().setLoadOld(false);
|
|
||||||
|
|
||||||
core.setOllamaObjectNoMemory(OllamaObject.builder()
|
|
||||||
.setModel("qwen3.5")
|
|
||||||
//.addMessage(new OllamaMessage(OllamaMessageRole.SYSTEM, "You are GLaDOS."))
|
|
||||||
.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("""
|
.addFact("""
|
||||||
@@ -117,15 +110,35 @@ public class Main implements Runnable {
|
|||||||
.loadBehaviorFromFile("Alice.txt")
|
.loadBehaviorFromFile("Alice.txt")
|
||||||
.capabilities(new LLMSystemPrompt.Capabilities()
|
.capabilities(new LLMSystemPrompt.Capabilities()
|
||||||
.use(AnnouncerVoiceCalling.class, "Use this to make an announcement. Not for regular messages."))
|
.use(AnnouncerVoiceCalling.class, "Use this to make an announcement. Not for regular messages."))
|
||||||
.build())
|
.outputFormat(new OutputFormat("""
|
||||||
.keep_alive(10)
|
When writing mathematical expressions, always wrap them in $...$ (inline) or $$...$$ (display), and use proper LaTeX commands (\\times, \\cdot) instead of * for multiplication. Never use bare * or _ outside of markdown emphasis or math delimiters."""))
|
||||||
.stream(true)
|
.build();
|
||||||
.setThinkingTier(OllamaObject.Thinking.FALSE)
|
|
||||||
.build());
|
Message prompt = identity.generateSystemPrompt();
|
||||||
|
|
||||||
|
pb = new PlaybackHandlerPiper();
|
||||||
|
pbk = new PlaybackHandlerKokoro();
|
||||||
|
|
||||||
|
Path socketPath = Paths.get("/tmp/whisper.sock");
|
||||||
|
|
||||||
|
Options.getInstance().setDataDir(Path.of("GLaDOS"), false);
|
||||||
|
|
||||||
|
core = new Core(pb);
|
||||||
|
|
||||||
|
LaunchOptions.getInstance().setLoadOld(false);
|
||||||
|
|
||||||
|
String llamaAddress = System.getenv().getOrDefault("LLAMA_ADDRESS", "http://127.0.0.1:8080");
|
||||||
|
|
||||||
|
core.setModelNoMemory(new OpenAIModel(llamaAddress, "unsloth/Qwen3.5", OpenAIThinking.LOW));
|
||||||
|
|
||||||
//core.addTool(new AnnouncerVoiceCalling(pb), Core.Source.CTP);
|
//core.addTool(new AnnouncerVoiceCalling(pb), Core.Source.CTP);
|
||||||
|
|
||||||
core.getOllamaObject().reGenerateSystemPrompt();
|
//core.getOllamaObject().reGenerateSystemPrompt();
|
||||||
|
|
||||||
|
core.addMessage(prompt);
|
||||||
|
|
||||||
|
APIApplication.start();
|
||||||
|
APIApplication.setInstence(this);
|
||||||
|
|
||||||
try(WhisperSocketServer server = new WhisperSocketServer(socketPath, event ->
|
try(WhisperSocketServer server = new WhisperSocketServer(socketPath, event ->
|
||||||
{
|
{
|
||||||
@@ -140,7 +153,7 @@ public class Main implements Runnable {
|
|||||||
event.getKey().runner_up(), event.getKey().runner_up_confidence() * 100,
|
event.getKey().runner_up(), event.getKey().runner_up_confidence() * 100,
|
||||||
event.getKey().text()) : event.getKey().text();
|
event.getKey().text()) : event.getKey().text();
|
||||||
|
|
||||||
sendAndHandleMessage(fixedMessage.toString(), true);
|
sendAndHandleMessage(fixedMessage.toString(), false);
|
||||||
}))
|
}))
|
||||||
{
|
{
|
||||||
server.start();
|
server.start();
|
||||||
@@ -164,6 +177,7 @@ public class Main implements Runnable {
|
|||||||
/corelist Lists all tools according to the OllamaObject.
|
/corelist Lists all tools according to the OllamaObject.
|
||||||
/working Prints the current working directories.
|
/working Prints the current working directories.
|
||||||
/peek Peeks the current memory.
|
/peek Peeks the current memory.
|
||||||
|
/speek Speaks out the last message again.
|
||||||
""");
|
""");
|
||||||
break;
|
break;
|
||||||
case "bye":
|
case "bye":
|
||||||
@@ -192,40 +206,39 @@ public class Main implements Runnable {
|
|||||||
case "list":
|
case "list":
|
||||||
writeLog("Tools installed in this instance");
|
writeLog("Tools installed in this instance");
|
||||||
|
|
||||||
for (Pair<OllamaFunctionTool, String> funtion : core.getFuntionTools()) {
|
for (Tool tool : core.getTools()) {
|
||||||
|
if(tool instanceof FunctionTool function) {
|
||||||
StringBuilder args = new StringBuilder();
|
StringBuilder args = new StringBuilder();
|
||||||
OllamaPerameter perameter = funtion.getKey().parameters();
|
ToolParameters perameter = function.parameters();
|
||||||
if (perameter != null) {
|
if (perameter != null) {
|
||||||
JSONObject obj = perameter.getProperties();
|
Map<String, ToolParameters.ToolParametersBuilder.Property> obj = perameter.getProperties();
|
||||||
for (String name : obj.keySet()) {
|
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);
|
args.append(args.toString().isBlank() ? "" : ", ").append(obj.get(name).type()).append(perameter.getRequired().stream().anyMatch(str -> str.equalsIgnoreCase(name)) ? "" : "?").append(" ").append(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
System.out.println("> Function: " + funtion.getKey().name() + "(" + args + ") [" + funtion.getValue() + "]");
|
System.out.println("> Function: " + function.name() + "(" + args + ")");
|
||||||
writeLog("Function: " + funtion.getKey().name() + "(" + args + ") [" + funtion.getValue() + "]");
|
writeLog("Function: " + function.name() + "(" + args + ")");
|
||||||
}
|
}
|
||||||
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;
|
break;
|
||||||
case "working":
|
case "working":
|
||||||
System.out.println("Working directories:\n" +
|
System.out.println("Working directories:\n" +
|
||||||
" Data: " + Core.DATA_DIR.getAbsolutePath() + "\n" +
|
" Data: " + Options.getInstance().getDataDir().getAbsolutePath() + "\n" +
|
||||||
" DateFiles: " + FileHandlerLocation.DATA_FILES + "\n" +
|
" DateFiles: " + Options.getInstance().getFileHandlerDataLocation() + "\n" +
|
||||||
" Plugins: " + Core.PLUGIN_DIRECTORY.getAbsolutePath());
|
" Plugins: " + Options.getInstance().getPluginDirectory().getAbsolutePath());
|
||||||
break;
|
break;
|
||||||
|
case "speek":
|
||||||
|
for(String str : chunker.appendAndExtract((String) core.getChatObject().getConversation().getLast().getContent()))
|
||||||
|
{
|
||||||
|
pbk.playVoice(stripMarkdownForTts(str));
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
System.out.println("Unknown command: " + message);
|
System.out.println("Unknown command: " + message);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
writeLog("User: " + message);
|
writeLog("User: " + message);
|
||||||
sendAndHandleMessage(message.toString(), true);
|
sendAndHandleMessage(message.toString(), false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -245,25 +258,96 @@ public class Main implements Runnable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public JSONObject sendAndHandleMessage(String message, boolean noVoice) {
|
private static final Pattern MATH_OR_CODE = Pattern.compile(
|
||||||
core.getOllamaObject().addMessage(new OllamaMessage(OllamaMessageRole.USER, message));
|
"```.*?```" // fenced code blocks (multiline)
|
||||||
return core.qurryOllama((noVoice?((_)->{}):onStreamChunkConsumer)).thenApply(json -> {
|
+ "|`[^`\\n]+?`" // inline code spans
|
||||||
|
+ "|\\$\\$.*?\\$\\$" // display math $$...$$
|
||||||
|
+ "|\\$[^$\\n]+?\\$" // inline math $...$
|
||||||
|
+ "|\\\\\\[.*?\\\\\\]" // display math \[...\]
|
||||||
|
+ "|\\\\\\(.*?\\\\\\)", // inline math \(...\)
|
||||||
|
Pattern.DOTALL
|
||||||
|
);
|
||||||
|
|
||||||
|
private static final Pattern MARKDOWN_EMPHASIS = Pattern.compile(
|
||||||
|
"\\*\\*\\*(.+?)\\*\\*\\*" // ***bold italic***
|
||||||
|
+ "|\\*\\*(.+?)\\*\\*" // **bold**
|
||||||
|
+ "|\\*(.+?)\\*" // *italic*
|
||||||
|
+ "|___(.+?)___" // ___bold italic___
|
||||||
|
+ "|__(.+?)__" // __bold__
|
||||||
|
+ "|_(.+?)_" // _italic_
|
||||||
|
+ "|~~(.+?)~~" // ~~strikethrough~~
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strips markdown emphasis syntax (*, _, **, __, ~~) from text intended for
|
||||||
|
* TTS, while leaving math and code regions untouched, since those characters
|
||||||
|
* are semantically meaningful there (multiplication, subscripts, etc.).
|
||||||
|
*/
|
||||||
|
public String stripMarkdownForTts(String input) {
|
||||||
|
// Step 1: pull out math/code spans, replace with placeholders
|
||||||
|
List<String> protectedSpans = new ArrayList<>();
|
||||||
|
Matcher protectMatcher = MATH_OR_CODE.matcher(input);
|
||||||
|
StringBuilder withPlaceholders = new StringBuilder();
|
||||||
|
int lastEnd = 0;
|
||||||
|
|
||||||
|
while (protectMatcher.find()) {
|
||||||
|
withPlaceholders.append(input, lastEnd, protectMatcher.start());
|
||||||
|
String placeholder = "\u0000" + protectedSpans.size() + "\u0000";
|
||||||
|
protectedSpans.add(protectMatcher.group());
|
||||||
|
withPlaceholders.append(placeholder);
|
||||||
|
lastEnd = protectMatcher.end();
|
||||||
|
}
|
||||||
|
withPlaceholders.append(input.substring(lastEnd));
|
||||||
|
|
||||||
|
// Step 2: strip markdown emphasis from what's left
|
||||||
|
String stripped = MARKDOWN_EMPHASIS.matcher(withPlaceholders.toString())
|
||||||
|
.replaceAll(mr -> {
|
||||||
|
for (int i = 1; i <= mr.groupCount(); i++) {
|
||||||
|
if (mr.group(i) != null) return mr.group(i);
|
||||||
|
}
|
||||||
|
return mr.group();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Step 3: restore protected spans
|
||||||
|
StringBuilder result = new StringBuilder(stripped);
|
||||||
|
for (int i = 0; i < protectedSpans.size(); i++) {
|
||||||
|
String placeholder = "\u0000" + i + "\u0000";
|
||||||
|
int idx = result.indexOf(placeholder);
|
||||||
|
if (idx != -1) {
|
||||||
|
result.replace(idx, idx + placeholder.length(), protectedSpans.get(i));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Message sendAndHandleMessage(String message, boolean noVoice) {
|
||||||
|
core.addMessage(new Message(Message.Role.USER, message));
|
||||||
|
try {
|
||||||
|
return core.queryModel((noVoice?((_)->{}):onStreamChunkConsumer)).thenApply(json -> {
|
||||||
|
//onStreamChunkConsumer.accept(json);
|
||||||
|
pbk.playVoice(stripMarkdownForTts(chunker.flush()));
|
||||||
core.handleResponse(json);
|
core.handleResponse(json);
|
||||||
return json;
|
return json;
|
||||||
}).join();
|
}).join();
|
||||||
|
} catch (OperationNotSupportedException e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void sendAndHandleMessageNonBlocking(String message)
|
public void sendAndHandleMessageNonBlocking(String message)
|
||||||
{
|
{
|
||||||
core.getOllamaObject().addMessage(new OllamaMessage(OllamaMessageRole.USER, message));
|
core.addMessage(new Message(Message.Role.USER, message));
|
||||||
core.qurryOllama(onStreamChunkConsumer).thenAccept(core::handleResponse);
|
try {
|
||||||
|
core.queryModel(onStreamChunkConsumer).thenAccept(core::handleResponse);
|
||||||
|
}catch (Exception _) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class PlaybackHandlerKokoro
|
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 KOKORO_82M_MODEL_FP16 = Path.of("~/.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 static final Path AF_HEART_VOICE = Path.of("~/.local/share/onnxruntime/voices/af_heart.bin");
|
||||||
|
|
||||||
public void playVoice(String msg)
|
public void playVoice(String msg)
|
||||||
{
|
{
|
||||||
@@ -353,10 +437,10 @@ public class Main implements Runnable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void printMessage(OllamaMessage ollamaMessage) {
|
public void printMessage(Message ollamaMessage) {
|
||||||
System.out.println(ollamaMessage.toJSON().toString());
|
System.out.println(ollamaMessage.toString());
|
||||||
|
|
||||||
String message = ollamaMessage.getContent();
|
String message = ollamaMessage.getContent().toString();
|
||||||
|
|
||||||
message = EmojiParser.parseToAliases(message).replace("::", ": :");
|
message = EmojiParser.parseToAliases(message).replace("::", ": :");
|
||||||
message = message.replaceAll(":(\\w+):", "$1").replace('_', ' ');
|
message = message.replaceAll(":(\\w+):", "$1").replace('_', ' ');
|
||||||
@@ -374,7 +458,7 @@ public class Main implements Runnable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void printErrorMessage(OllamaMessage ollamaMessage) {
|
public void printErrorMessage(Message ollamaMessage) {
|
||||||
System.out.println("Error: "+ollamaMessage.getContent());
|
System.out.println("Error: "+ollamaMessage.getContent());
|
||||||
playAnnouncer("Error: "+ollamaMessage.getContent());
|
playAnnouncer("Error: "+ollamaMessage.getContent());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,53 +9,57 @@ public class SentenceChunker {
|
|||||||
|
|
||||||
private final StringBuilder buffer = new StringBuilder();
|
private final StringBuilder buffer = new StringBuilder();
|
||||||
|
|
||||||
// Matches a sentence-ending punctuation mark followed by whitespace (or end of buffer),
|
// Sentence-ending punctuation followed by real whitespace (not end-of-buffer, since more
|
||||||
// but only greedily captures the punctuation itself so we can inspect context around it.
|
// text might still arrive: "3." + "14"), or a line break.
|
||||||
private static final Pattern BOUNDARY = Pattern.compile("[.!?]+[\"')\\]]?(?=\\s|$)");
|
private static final Pattern BOUNDARY = Pattern.compile("[.!?]+[\"')\\]]?(?=\\s)|\\n");
|
||||||
|
|
||||||
|
// Just a 1-3 digit number (optionally indented): the text before the dot in "1." / "12."
|
||||||
|
private static final Pattern LIST_MARKER = Pattern.compile("\\s*\\d{1,3}");
|
||||||
|
|
||||||
|
private static final String[] ABBREVIATIONS = {"Mr", "Mrs", "Ms", "Dr", "vs", "etc", "e.g", "i.e"};
|
||||||
|
|
||||||
/**
|
|
||||||
* 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) {
|
public List<String> appendAndExtract(String delta) {
|
||||||
buffer.append(delta);
|
buffer.append(delta);
|
||||||
List<String> sentences = new ArrayList<>();
|
List<String> sentences = new ArrayList<>();
|
||||||
|
|
||||||
Matcher m = BOUNDARY.matcher(buffer);
|
Matcher m = BOUNDARY.matcher(buffer);
|
||||||
int lastCut = 0;
|
int sentenceStart = 0; // where the current sentence begins
|
||||||
|
int searchFrom = 0; // where to resume scanning
|
||||||
|
|
||||||
while (m.find(lastCut)) {
|
while (m.find(searchFrom)) {
|
||||||
int boundaryEnd = m.end();
|
int start = m.start();
|
||||||
|
int end = m.end();
|
||||||
|
searchFrom = end;
|
||||||
|
|
||||||
if (isFalsePositive(buffer, m.start(), boundaryEnd)) {
|
if (isFalsePositive(buffer, start, end, sentenceStart)) continue;
|
||||||
lastCut = boundaryEnd; // skip past it, keep scanning the same buffer
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
String sentence = buffer.substring(lastCut, boundaryEnd).trim();
|
String sentence = buffer.substring(sentenceStart, end).trim();
|
||||||
if (!sentence.isEmpty()) sentences.add(sentence);
|
if (!sentence.isEmpty()) sentences.add(sentence);
|
||||||
lastCut = boundaryEnd;
|
sentenceStart = end;
|
||||||
}
|
}
|
||||||
|
|
||||||
buffer.delete(0, lastCut); // keep only the unconsumed remainder
|
buffer.delete(0, sentenceStart); // keep only the unconsumed remainder
|
||||||
return sentences;
|
return sentences;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Call once generation is done to flush whatever's left, even without terminal punctuation. */
|
|
||||||
public String flush() {
|
public String flush() {
|
||||||
String remainder = buffer.toString().trim();
|
String remainder = buffer.toString().trim();
|
||||||
buffer.setLength(0);
|
buffer.setLength(0);
|
||||||
return remainder;
|
return remainder;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isFalsePositive(CharSequence buf, int start, int end) {
|
private boolean isFalsePositive(StringBuilder buf, int start, int end, int sentenceStart) {
|
||||||
// Guard: decimal numbers, e.g. "3.14" — digit immediately before and after the dot
|
if (buf.charAt(start) != '.') return false; // only dots are ambiguous
|
||||||
if (buf.charAt(start) == '.' && start > 0 && Character.isDigit(buf.charAt(start - 1))) {
|
|
||||||
if (end < buf.length() && Character.isDigit(buf.charAt(end))) return true;
|
// Numbered list marker: a bare "1." at the start of a line (or of the current sentence)
|
||||||
|
if (end - start == 1) {
|
||||||
|
int lineStart = Math.max(sentenceStart, buf.lastIndexOf("\n", start - 1) + 1);
|
||||||
|
if (LIST_MARKER.matcher(buf.subSequence(lineStart, start)).matches()) return true;
|
||||||
}
|
}
|
||||||
// Guard: common abbreviations right before the dot (extend as needed)
|
|
||||||
String before = buf.subSequence(Math.max(0, start - 4), start).toString();
|
// Abbreviations right before the dot
|
||||||
for (String abbr : new String[]{"Mr", "Mrs", "Ms", "Dr", "vs", "etc", "e.g", "i.e"}) {
|
String before = buf.substring(Math.max(0, start - 4), start);
|
||||||
|
for (String abbr : ABBREVIATIONS) {
|
||||||
if (before.endsWith(abbr)) return true;
|
if (before.endsWith(abbr)) return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ 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 me.neurodock.core.Pair;
|
||||||
import org.json.JSONObject;
|
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
import java.io.BufferedReader;
|
||||||
import java.io.BufferedWriter;
|
import java.io.BufferedWriter;
|
||||||
@@ -25,7 +24,7 @@ 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 timestamp, String language,
|
||||||
double confidence, String runner_up, double runner_up_confidence) {
|
double confidence, String runner_up, double runner_up_confidence) {
|
||||||
public boolean isAmbiguous(double minGap)
|
public boolean isAmbiguous(double minGap)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,10 +4,9 @@ import io.swagger.v3.oas.annotations.media.Content;
|
|||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
import io.swagger.v3.oas.annotations.responses.ApiResponses;
|
||||||
|
import me.neurodock.backend.open.ai.OpenAIAssistentMessage;
|
||||||
import me.neurodock.glados.rest.APIApplication;
|
import me.neurodock.glados.rest.APIApplication;
|
||||||
import me.neurodock.glados.rest.payloads.MessagePayload;
|
import me.neurodock.glados.rest.payloads.MessagePayload;
|
||||||
import me.neurodock.ollama.OllamaMessageRole;
|
|
||||||
import org.json.JSONObject;
|
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
@@ -38,13 +37,28 @@ public ResponseEntity<?> send(@RequestBody MessagePayload payload)
|
|||||||
return ResponseEntity.ok("success");
|
return ResponseEntity.ok("success");
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
JSONObject body = APIApplication.getInstence().sendAndHandleMessage(payload.message(), payload.noVoice());
|
me.neurodock.llm.Message message = APIApplication.getInstence().sendAndHandleMessage(payload.message(), payload.noVoice());
|
||||||
MessagePayload response = new MessagePayload(
|
MessagePayload response;
|
||||||
body.getJSONObject("message").optString("content", ""),
|
if(message instanceof OpenAIAssistentMessage msg)
|
||||||
OllamaMessageRole.ASSISTANT,
|
{
|
||||||
body.optBoolean("done", false),
|
response = new MessagePayload(
|
||||||
|
(String) message.getContent(),
|
||||||
|
me.neurodock.llm.Message.Role.ASSISTANT,
|
||||||
|
true,
|
||||||
payload.noVoice(),
|
payload.noVoice(),
|
||||||
false);
|
false,
|
||||||
|
msg.getEndReason());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
response = new MessagePayload(
|
||||||
|
(String) message.getContent(),
|
||||||
|
me.neurodock.llm.Message.Role.ASSISTANT,
|
||||||
|
true,
|
||||||
|
payload.noVoice(),
|
||||||
|
false,
|
||||||
|
OpenAIAssistentMessage.Reason.NULL);
|
||||||
|
}
|
||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(response);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,8 +71,11 @@ public ResponseEntity<?> send(@RequestBody MessagePayload payload)
|
|||||||
{
|
{
|
||||||
ArrayList<MessagePayload> body = new ArrayList<>();
|
ArrayList<MessagePayload> body = new ArrayList<>();
|
||||||
|
|
||||||
APIApplication.getInstence().getCore().getOllamaObject().getMessages().forEach(mess -> {
|
APIApplication.getInstence().getCore().getChatObject().getConversation().forEach(mess -> {
|
||||||
body.add(new MessagePayload(mess.getContent(), mess.getRole(), false, false, false));
|
if (mess instanceof OpenAIAssistentMessage msg)
|
||||||
|
body.add(new MessagePayload((String) mess.getContent(), mess.getRole(), false, false, false, msg.getEndReason()));
|
||||||
|
else
|
||||||
|
body.add(new MessagePayload((String) mess.getContent(), mess.getRole(), false, false, false, OpenAIAssistentMessage.Reason.STOP));
|
||||||
});
|
});
|
||||||
|
|
||||||
return ResponseEntity.ok(body);
|
return ResponseEntity.ok(body);
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package me.neurodock.glados.rest.payloads;
|
package me.neurodock.glados.rest.payloads;
|
||||||
|
|
||||||
import me.neurodock.ollama.OllamaMessageRole;
|
|
||||||
|
|
||||||
public record MessagePayload(String message, OllamaMessageRole role, boolean finished, boolean noVoice, boolean stream) {
|
import me.neurodock.backend.open.ai.OpenAIAssistentMessage;
|
||||||
|
import me.neurodock.llm.Message;
|
||||||
|
|
||||||
|
public record MessagePayload(String message, Message.Role role, boolean finished, boolean noVoice, boolean stream,
|
||||||
|
OpenAIAssistentMessage.Reason endReason) {
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user