added multimedia support and STT. STT and photos are have bugs but are functional. file upload is conceptualized but will require additional backend work. video is on hold for now.

This commit is contained in:
Joshua
2024-07-10 19:15:03 -06:00
parent e5b69aa2b6
commit cbd040f57b
36 changed files with 742 additions and 137 deletions
+231
View File
@@ -0,0 +1,231 @@
import 'dart:convert';
import 'dart:io';
import 'dart:typed_data';
import 'package:flutter/widgets.dart';
import 'package:app/components/conversation_thread/conversation.dart';
import 'package:camera/camera.dart';
import 'package:http/http.dart' as http;
import 'package:app/components/conversation_thread/chat.dart';
import 'package:app/components/util/speech_to_text.dart';
import 'model.dart';
// Uses Singleton design pattern to ensure that any server references across the app are using the same instance.
class Backend {
// SERVER VARIABLES
static final Backend singleBackend = Backend._internal();
Backend._internal();
// TODO: One-time process to establish server connection is required for easy setup/maintenance.
static const url = "http://10.0.2.2:11434/api/"; // For emulated device in testing
//static final url = "http://192.168.1.68:11434/api"; // For local access to home server
static String endpoint = "";
static final Map<String, String> headers = {'Content-Type': 'application/json'};
static final bool UI_TESTING = true;
// AI VARIABLES
static final SpeechToText stt = SpeechToText();
static late bool _speechEnabled;
// PRIVACY-NECESSARY VARIABLES
// TODO: Reorder based on most recent usage.
static List<Conversation> conversations = [];
static List<Chat> loadedChats = [];
static late Conversation loadedConversation;
static late List<CameraDescription> cameras;
static late CameraController controller;
// RUNTIME & STATUS VARIABLES
static late int conversationID;
static late int maxConversationID;
static late Model model;
static String prompt = "";
static String response = "";
static bool initiated = false;
// TODO: Verify all different components of STT, TTS, LLM, etc have been initialized.
// TODO: Handshake w/ server to validate identity
factory Backend() {
return singleBackend;
}
void init() async {
// Prevent multiple initiations
if(!initiated) {
initiated = true;
// Speech-To-Text engine activation.
_speechEnabled = stt.isSpeechEnabled();
model = Model("llama3"); // Primarily used llama3, testing gemma2
maxConversationID = conversations.length;
cameras = await availableCameras();
controller = CameraController(cameras[0], ResolutionPreset.max);
controller.initialize(); // TODO: Add error catching on camera
}
// TODO: Dynamically get available models and models that can be pulled.
// TODO: Dynamically get the user's default model
// TODO: Dynamically obtain conversations.
// TODO: Obtain all user permissions at once.
}
// PRIMARY INTERFACES
@Deprecated("Use STT interface directly and use respondWhenReady().")
static Future<String> sttGetResponseWhenReady() async {
// TODO: In case on-device STT is unavailable, use server-side STT service.
if (!_speechEnabled) {
return "Unable to process Speech-To-Text on-device.";
}
prompt = stt.getTextWhenReady() as String;
conversations[conversationID].add(Chat(prompt, true, 0));
if(!UI_TESTING) {
httpSendRequest();
}
conversations[conversationID].add(Chat(response, false, 0));
return response;
}
static void respondWhenReady(String p) async {
prompt = p;
conversations[conversationID].add(Chat(prompt, true, 0));
if(!UI_TESTING) {
httpSendRequest();
}
}
static ImageProvider convertFromBase64(img) {
Uint8List imageBytes = base64Decode(img);
return Image.memory(imageBytes).image;
}
// Converts image to base64 encoded string for data transfer.
static String convertToBase64(f) {
List<int> imageBytes = f.readAsBytesSync();
String img = base64Encode(imageBytes);
// TODO: Add special handling for images included in prompts.
conversations[conversationID].add(Chat.image(img, true, 1));
return img;
}
// CONVERSATION MANAGEMENT
@Deprecated("")
static List<Conversation> getConversationsList() { return conversations; }
static Conversation getLoadedConversation() { return loadedConversation; }
// Load conversation should be called prior to any other server requests.
static void loadConversation(id) {
conversationID = id;
loadedConversation = conversations[id];
loadedChats = loadedConversation.chats;
}
// TODO: Better conversation ID management is needed. Preferably randomized IDs to not indicate the number of conversations.
static int createConversation() {
int id = conversations.length;
conversations.add(Conversation(id, "Conversation $id", "Another AI conversation"));
return id;
}
static void deleteConversation(id) {
loadedChats = conversations[conversationID].chats; // TODO: Temporarily saving the chats for "undo" popup.
conversations.removeAt(id);
}
@Deprecated('Just directly get loadedChats list. Unless multiple chats can be opened simultaneously?')
static List<Chat> getChatsByConversationID(id) { return conversations[id].chats; }
// SERVER UTILS
static List<Map<String, String>> convertMessages() {
// TODO: Integrate with getConversationByID()
List<Map<String, String>> messageList = [];
for (Chat c in loadedChats) {
messageList.add({
"role": c.role ? "user" : "assistant",
"content": c.content,
});
}
return messageList;
}
// Formats the prompt in JSON.
static String constructPrompt() {
// TODO: Allow for additional flags, ie. continuous conversation, images, etc.
// TODO: Allow for toggleable states between stream
// TODO: Allow for different models to be dynamically selected.
Map<String, dynamic> data;
if(conversations[conversationID].conversationContext) {
loadedChats = [...loadedChats, Chat(prompt, true, 0)];
endpoint = "chat";
List<Map<String, String>> messageList = convertMessages();
data = {
"model": model.name,
"messages": messageList,
"stream": loadedConversation.stream,
};
}
else {
endpoint = "generate";
data = {
"model": model.name,
"prompt": prompt,
"stream": loadedConversation.stream,
};
}
// Encode the JSON payload
return json.encode(data);
}
// Send the HTTP request to the server.
static void httpSendRequest() async {
// TODO: Add AES256 encryption to the prompt/messages thread.
// TODO: Add SHA256 checksum to be sent and verified to prevent/notify in case of data loss.
String json = constructPrompt();
// Send the HTTP POST request
final serverResponse = await http.post(
Uri.parse('$url$endpoint'),
headers: headers,
body: json,
);
if (serverResponse.statusCode == 200) {
Map<String, dynamic> re = jsonDecode(serverResponse.body);
if(conversations[conversationID].conversationContext) {
response = re["message"]["content"];
// TODO: Modify in case of different response types.
loadedChats = [...loadedChats, Chat(response, false, 0)];
} else {
response = re["response"];
}
}
conversations[conversationID].add(Chat(response, false, 0));
prompt = "";
response = "";
}
}
@@ -0,0 +1,5 @@
class Model {
late String name;
Model(this.name);
}
@@ -0,0 +1,59 @@
import 'package:speech_to_text/speech_recognition_result.dart';
import 'package:speech_to_text/speech_to_text.dart' as stt;
import 'package:speech_to_text/speech_to_text.dart';
/// Handles standard Speech-To-Text using on-device processing.
class SpeechToText {
final stt.SpeechToText speechToText = stt.SpeechToText();
// TODO: User customizable STT??
final SpeechListenOptions options = stt.SpeechListenOptions(
partialResults: true,
onDevice: true,
);
bool speechEnabled = false;
String text = "";
// Check if speech recognition is enabled.
bool isSpeechEnabled() => speechEnabled;
Future<String> getTextWhenReady() async {
await speechToText.stop();
return text;
}
// Retrieve the text.
@Deprecated("TODO: Deprecated message for speechToText.getText")
String getText() => text;
// Reset either after saving the text or prior to using STT again.
void reset() => text = "";
/// Start a speech recognition session
void startListening() async {
await speechToText.listen(
onResult: onSpeechResult,
pauseFor: const Duration(seconds: 3),
// TODO: I think this is a bug with the speech_to_text library? The docs correctly identify the named parameter of SpeechListenOptions, but it's not recognized.
// SpeechListenOptions: options,
);
}
/// Manually stop the active speech recognition session
/// Note that there are also timeouts that each platform enforces
/// and the SpeechToText plugin supports setting timeouts on the
/// listen method.
void stopListening() async => await speechToText.stop();
SpeechToText() {
initSpeech();
}
/// This has to happen only once per app
void initSpeech() async => speechEnabled = await speechToText.initialize();
/// This is the callback that the SpeechToText plugin calls when
/// the platform returns recognized words.
void onSpeechResult(SpeechRecognitionResult result) => (text = result.recognizedWords);
}
@@ -0,0 +1,317 @@
import 'dart:math';
class UsageQueue<T> implements List {
@override
var first;
@override
var last;
@override
late int length;
@override
List operator +(List other) {
// TODO: implement +
throw UnimplementedError();
}
@override
operator [](int index) {
// TODO: implement []
throw UnimplementedError();
}
@override
void operator []=(int index, value) {
// TODO: implement []=
}
@override
void add(value) {
// TODO: implement add
}
@override
void addAll(Iterable iterable) {
// TODO: implement addAll
}
@override
bool any(bool Function(dynamic element) test) {
// TODO: implement any
throw UnimplementedError();
}
@override
Map<int, dynamic> asMap() {
// TODO: implement asMap
throw UnimplementedError();
}
@override
List<R> cast<R>() {
// TODO: implement cast
throw UnimplementedError();
}
@override
void clear() {
// TODO: implement clear
}
@override
bool contains(Object? element) {
// TODO: implement contains
throw UnimplementedError();
}
@override
elementAt(int index) {
// TODO: implement elementAt
throw UnimplementedError();
}
@override
bool every(bool Function(dynamic element) test) {
// TODO: implement every
throw UnimplementedError();
}
@override
Iterable<T> expand<T>(Iterable<T> Function(dynamic element) toElements) {
// TODO: implement expand
throw UnimplementedError();
}
@override
void fillRange(int start, int end, [fillValue]) {
// TODO: implement fillRange
}
@override
firstWhere(bool Function(dynamic element) test, {Function()? orElse}) {
// TODO: implement firstWhere
throw UnimplementedError();
}
@override
T fold<T>(T initialValue, T Function(T previousValue, dynamic element) combine) {
// TODO: implement fold
throw UnimplementedError();
}
@override
Iterable followedBy(Iterable other) {
// TODO: implement followedBy
throw UnimplementedError();
}
@override
void forEach(void Function(dynamic element) action) {
// TODO: implement forEach
}
@override
Iterable getRange(int start, int end) {
// TODO: implement getRange
throw UnimplementedError();
}
@override
int indexOf(element, [int start = 0]) {
// TODO: implement indexOf
throw UnimplementedError();
}
@override
int indexWhere(bool Function(dynamic element) test, [int start = 0]) {
// TODO: implement indexWhere
throw UnimplementedError();
}
@override
void insert(int index, element) {
// TODO: implement insert
}
@override
void insertAll(int index, Iterable iterable) {
// TODO: implement insertAll
}
@override
// TODO: implement isEmpty
bool get isEmpty => throw UnimplementedError();
@override
// TODO: implement isNotEmpty
bool get isNotEmpty => throw UnimplementedError();
@override
// TODO: implement iterator
Iterator get iterator => throw UnimplementedError();
@override
String join([String separator = ""]) {
// TODO: implement join
throw UnimplementedError();
}
@override
int lastIndexOf(element, [int? start]) {
// TODO: implement lastIndexOf
throw UnimplementedError();
}
@override
int lastIndexWhere(bool Function(dynamic element) test, [int? start]) {
// TODO: implement lastIndexWhere
throw UnimplementedError();
}
@override
lastWhere(bool Function(dynamic element) test, {Function()? orElse}) {
// TODO: implement lastWhere
throw UnimplementedError();
}
@override
Iterable<T> map<T>(T Function(dynamic e) toElement) {
// TODO: implement map
throw UnimplementedError();
}
@override
reduce(Function(dynamic value, dynamic element) combine) {
// TODO: implement reduce
throw UnimplementedError();
}
@override
bool remove(Object? value) {
// TODO: implement remove
throw UnimplementedError();
}
@override
removeAt(int index) {
// TODO: implement removeAt
throw UnimplementedError();
}
@override
removeLast() {
// TODO: implement removeLast
throw UnimplementedError();
}
@override
void removeRange(int start, int end) {
// TODO: implement removeRange
}
@override
void removeWhere(bool Function(dynamic element) test) {
// TODO: implement removeWhere
}
@override
void replaceRange(int start, int end, Iterable replacements) {
// TODO: implement replaceRange
}
@override
void retainWhere(bool Function(dynamic element) test) {
// TODO: implement retainWhere
}
@override
// TODO: implement reversed
Iterable get reversed => throw UnimplementedError();
@override
void setAll(int index, Iterable iterable) {
// TODO: implement setAll
}
@override
void setRange(int start, int end, Iterable iterable, [int skipCount = 0]) {
// TODO: implement setRange
}
@override
void shuffle([Random? random]) {
// TODO: implement shuffle
}
@override
// TODO: implement single
get single => throw UnimplementedError();
@override
singleWhere(bool Function(dynamic element) test, {Function()? orElse}) {
// TODO: implement singleWhere
throw UnimplementedError();
}
@override
Iterable skip(int count) {
// TODO: implement skip
throw UnimplementedError();
}
@override
Iterable skipWhile(bool Function(dynamic value) test) {
// TODO: implement skipWhile
throw UnimplementedError();
}
@override
void sort([int Function(dynamic a, dynamic b)? compare]) {
// TODO: implement sort
}
@override
List sublist(int start, [int? end]) {
// TODO: implement sublist
throw UnimplementedError();
}
@override
Iterable take(int count) {
// TODO: implement take
throw UnimplementedError();
}
@override
Iterable takeWhile(bool Function(dynamic value) test) {
// TODO: implement takeWhile
throw UnimplementedError();
}
@override
List toList({bool growable = true}) {
// TODO: implement toList
throw UnimplementedError();
}
@override
Set toSet() {
// TODO: implement toSet
throw UnimplementedError();
}
@override
Iterable where(bool Function(dynamic element) test) {
// TODO: implement where
throw UnimplementedError();
}
@override
Iterable<T> whereType<T>() {
// TODO: implement whereType
throw UnimplementedError();
}
}