You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
327 lines
11 KiB
327 lines
11 KiB
#include "ofTeleprompter.h"
|
|
|
|
void ofTeleprompter::setup() {
|
|
|
|
ofBackground(255);
|
|
ofSetVerticalSync(false);
|
|
setupGUI();
|
|
|
|
/* load both texts */
|
|
loadText(script, filePath);
|
|
loadText(scriptContemporary, filePathContemp);
|
|
|
|
activeScript = &script;
|
|
|
|
textFont.load("Roboto-SemiBold.ttf", 24);
|
|
detailsFont.load("Roboto-SemiBold.ttf", 22);
|
|
|
|
// Prepare first line for teleprompter
|
|
currentSentence = (*activeScript)[currentLine].sentence;
|
|
displayedSentence.clear();
|
|
currentLetterIndex = 0;
|
|
lastWordTime = ofGetElapsedTimeMillis();
|
|
|
|
// Setup the LLMThread
|
|
llmThread.setup("http://localhost:8000/generate");
|
|
}
|
|
|
|
void ofTeleprompter::update() {
|
|
|
|
if(ofGetFrameNum() < 2) {
|
|
currentSpeaker = (*activeScript)[currentLine].speaker;
|
|
currentEmotion = (*activeScript)[currentLine].emotion;
|
|
currentSentence = (*activeScript)[currentLine].sentence;
|
|
}
|
|
|
|
currentLineIndex = ofToString(currentLine + 1) + " / " + ofToString(script.size() + 1);
|
|
|
|
// Teleprompter logic (letter by letter)
|
|
if (currentLetterIndex < currentSentence.size()) {
|
|
uint64_t now = ofGetElapsedTimeMillis();
|
|
if (now - lastWordTime > wordDelay) {
|
|
displayedSentence += currentSentence[currentLetterIndex];
|
|
currentLetterIndex++;
|
|
lastWordTime = now;
|
|
}
|
|
}
|
|
|
|
// Waits for llm thread to send a response before displaying!
|
|
if (waitingForLLM && llmThread.isResultReady()) {
|
|
llmResponse = llmThread.getResult();
|
|
if (llmResponse.empty()) {
|
|
ofLogError() << "LLM response is empty!";
|
|
} else {
|
|
ofJson json = ofJson::parse(llmResponse);
|
|
std::string responseText = json.value("response", "");
|
|
size_t start = responseText.find('(');
|
|
size_t end = responseText.find(')');
|
|
std::string speaker, sentence;
|
|
|
|
if (start != std::string::npos && end != std::string::npos && end > start) {
|
|
speaker = responseText.substr(start + 1, end - start - 1);
|
|
size_t colon = responseText.find(':', end);
|
|
if (colon != std::string::npos) {
|
|
sentence = responseText.substr(colon + 1);
|
|
sentence.erase(0, sentence.find_first_not_of(" \t"));
|
|
}
|
|
}
|
|
|
|
ofLog() << speaker;
|
|
currentSentence = sentence;
|
|
currentSpeaker = speaker;
|
|
currentEmotion = currentEmotionDetetced;
|
|
displayedSentence.clear();
|
|
currentLetterIndex = 0;
|
|
lastWordTime = ofGetElapsedTimeMillis();
|
|
waitingForLLM = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ofTeleprompter::draw() {
|
|
gui.draw();
|
|
drawText();
|
|
|
|
}
|
|
|
|
void ofTeleprompter::setupGUI() {
|
|
nextLine.addListener(this, &ofTeleprompter::nextLinePressed);
|
|
reset.addListener(this, &ofTeleprompter::resetScript);
|
|
|
|
useLLMOnly.addListener(this, &ofTeleprompter::toggleOffText);
|
|
useTextOnly.addListener(this, &ofTeleprompter::toggleOffLLM);
|
|
useContempTextOnly.addListener(this, &ofTeleprompter::toggleContempScript);
|
|
|
|
gui.setDefaultWidth(400);
|
|
gui.setup();
|
|
gui.add(currentLineIndex.setup("Current Line Index", "NULL"));
|
|
gui.add(currentSpeaker.setup("Current Speaker", "NULL"));
|
|
gui.add(currentEmotion.setup("Current Emotion", "NULL"));
|
|
gui.add(facesDetected.setup("Faces Detected", "NULL"));
|
|
gui.add(emotionIntensity.setup("Intensity", "NULL"));
|
|
gui.add(emotionDetected.setup("Emotion Detected", "NULL"));
|
|
gui.add(temperature.setup("Temperature", 0.7, 0, 1.5));
|
|
gui.add(useLLMOnly.setup("Use LLM Only", false));
|
|
gui.add(useTextOnly.setup("Use Text Only", false));
|
|
gui.add(useContempTextOnly.setup("Use Contept Text Only", false));
|
|
gui.add(useGeneratedFeedback.setup("Use LLM Feedback", false));
|
|
gui.add(nextLine.setup("Next Line"));
|
|
gui.add(reset.setup("Reset Script"));
|
|
}
|
|
|
|
void ofTeleprompter::loadText(std::vector<Line> & _script, std::string & _file) {
|
|
|
|
_script.clear();
|
|
ofFile jsonFile(_file);
|
|
if(jsonFile.exists()) {
|
|
ofJson json = ofLoadJson(jsonFile);
|
|
int idx = 0;
|
|
for (const auto& entry : json) {
|
|
Line l;
|
|
int randomIdx = ofRandom(7);
|
|
l.idx = idx++;
|
|
l.speaker = entry.value("first_speaker", "");
|
|
l.sentence = entry.value("first_text", "");
|
|
l.emotion = entry.value("first_emotion", "");
|
|
_script.push_back(l);
|
|
}
|
|
} else {
|
|
ofLogError() << "JSON file not found: " << filePath;
|
|
}
|
|
|
|
// Random Check
|
|
if (!_script.empty()) {
|
|
int randomIdx = ofRandom(_script.size()); // returns float
|
|
int idx = static_cast<int>(randomIdx); // convert to int
|
|
ofLog() << "Random line: " << _script[idx].speaker << ": " << _script[idx].sentence;
|
|
ofLog() << "Number of lines: " << _script.size();
|
|
}
|
|
|
|
|
|
// Set initial current line
|
|
currentLine = 0;
|
|
}
|
|
|
|
void ofTeleprompter::drawText() {
|
|
ofSetColor(ofColor::red);
|
|
|
|
// --- Display speaker and emotion centered at the top ---
|
|
std::string speakerText = "Speaker: " + currentSpeaker.getParameter().toString();
|
|
std::string emotionText = "Emotion: " + currentEmotion.getParameter().toString();
|
|
|
|
ofRectangle speakerBox = detailsFont.getStringBoundingBox(speakerText, 0, 0);
|
|
float speakerX = (ofGetWidth() - speakerBox.width) / 2.0f;
|
|
float speakerY = 128; // Top margin
|
|
|
|
ofRectangle emotionBox = detailsFont.getStringBoundingBox(emotionText, 0, 0);
|
|
float emotionX = (ofGetWidth() - emotionBox.width) / 2.0f;
|
|
float emotionY = speakerY + speakerBox.height + 10; // 10px below speaker
|
|
|
|
detailsFont.drawString(speakerText, speakerX, speakerY);
|
|
detailsFont.drawString(emotionText, emotionX, emotionY);
|
|
|
|
// -------
|
|
|
|
ofSetColor(ofColor::black);
|
|
float margin = 128; // pixels
|
|
float maxWidth = ofGetWidth() - margin * 2;
|
|
std::string wrapped = wrapStringToWidth(displayedSentence, maxWidth);
|
|
|
|
// Split wrapped into lines
|
|
std::vector<std::string> lines;
|
|
std::istringstream iss(wrapped);
|
|
std::string line;
|
|
while (std::getline(iss, line)) {
|
|
lines.push_back(line);
|
|
}
|
|
|
|
// Calculate total height for vertical centering
|
|
float totalHeight = lines.size() * textFont.getLineHeight();
|
|
float startY = (ofGetHeight() / 2.0f) - (totalHeight / 2.0f);
|
|
|
|
// Draw each line centered horizontally
|
|
for (size_t i = 0; i < lines.size(); ++i) {
|
|
ofRectangle bbox = textFont.getStringBoundingBox(lines[i], 0, 0);
|
|
float x = 128;//(ofGetWidth() - bbox.width) / 2.0f;
|
|
float y = startY + i * textFont.getLineHeight();
|
|
textFont.drawString(lines[i], x, y);
|
|
}
|
|
}
|
|
|
|
std::string ofTeleprompter::wrapStringToWidth(const std::string& text, float maxWidth) {
|
|
std::istringstream iss(text);
|
|
std::string word;
|
|
std::string wrapped, line;
|
|
while (iss >> word) {
|
|
std::string testLine = line.empty() ? word : line + " " + word;
|
|
ofRectangle bbox = textFont.getStringBoundingBox(testLine, 0, 0);
|
|
if (bbox.width > maxWidth && !line.empty()) {
|
|
wrapped += line + "\n";
|
|
line = word;
|
|
} else {
|
|
line = testLine;
|
|
}
|
|
}
|
|
if (!line.empty()) wrapped += line;
|
|
return wrapped;
|
|
}
|
|
|
|
void ofTeleprompter::updateCVData(int numOfFacesDetected, std::string emotion, float intensity) {
|
|
emotionDetected = emotion;
|
|
currentEmotionDetetced = emotion;
|
|
currentEmotionIntensity = intensity;
|
|
|
|
// Debug Values
|
|
facesDetected = ofToString(numOfFacesDetected);
|
|
emotionIntensity = ofToString(intensity);
|
|
}
|
|
|
|
void ofTeleprompter::nextLinePressed() {
|
|
|
|
// Check if llm thread is already running
|
|
if (waitingForLLM) {
|
|
ofLogWarning() << "LLM is still generating. Please wait.";
|
|
return;
|
|
}
|
|
|
|
ofLog() << "Next Line!";
|
|
|
|
if (currentLine < script.size()) {
|
|
currentLine++;
|
|
}
|
|
|
|
// If values reach a certain threshold or LLM only is on, and useTextOnly is false -> request a reponse from the llm
|
|
if (((currentEmotionIntensity > 0.8 && currentEmotionDetetced != "neutral") || useLLMOnly) && !useTextOnly) {
|
|
ofLog() << "Generate Line!";
|
|
|
|
std::string speaker = (*activeScript)[currentLine - 1].speaker;
|
|
std::string sentence = (*activeScript)[currentLine - 1].sentence;
|
|
std::string emotion = (*activeScript)[currentLine].emotion;
|
|
|
|
if (useGeneratedFeedback) {
|
|
speaker = currentSpeaker;
|
|
sentence = currentSentence;
|
|
emotion = currentEmotion;
|
|
|
|
ofLog() << "Using Generated Feedback";
|
|
}
|
|
|
|
llmThread.requestPrompt(speaker, sentence, currentEmotionDetetced, temperature);
|
|
waitingForLLM = true;
|
|
// Don't set currentSentence yet!
|
|
} else {
|
|
currentSpeaker = (*activeScript)[currentLine].speaker;
|
|
currentEmotion = (*activeScript)[currentLine].emotion;
|
|
currentSentence = (*activeScript)[currentLine].sentence;
|
|
displayedSentence.clear();
|
|
currentLetterIndex = 0;
|
|
lastWordTime = ofGetElapsedTimeMillis();
|
|
}
|
|
}
|
|
|
|
void ofTeleprompter::resetScript() {
|
|
// Need to reset the text, id, etc.
|
|
ofLog() << "Reset script.";
|
|
currentLine = 0;
|
|
|
|
// Prepare teleprompter effect for letter-by-letter
|
|
currentSpeaker = (*activeScript)[currentLine].speaker;
|
|
currentEmotion = (*activeScript)[currentLine].emotion;
|
|
currentSentence = (*activeScript)[currentLine].sentence;
|
|
|
|
displayedSentence.clear();
|
|
currentLetterIndex = 0;
|
|
lastWordTime = ofGetElapsedTimeMillis();
|
|
}
|
|
|
|
void ofTeleprompter::toggleOffLLM(bool & val) {
|
|
if (val) {
|
|
useLLMOnly = false;
|
|
}
|
|
}
|
|
|
|
void ofTeleprompter::toggleOffText(bool & val) {
|
|
if (val) {
|
|
useTextOnly = false;
|
|
}
|
|
}
|
|
|
|
void ofTeleprompter::toggleContempScript(bool & val) {
|
|
if (val) {
|
|
activeScript = &scriptContemporary;
|
|
} else {
|
|
activeScript = &script;
|
|
}
|
|
|
|
ofLog() << "Script Size:" + (*activeScript).size();
|
|
}
|
|
|
|
void ofTeleprompter::keyPressed(int key){
|
|
if(key == 'f' || key == 'F'){
|
|
ofToggleFullscreen();
|
|
}
|
|
|
|
if(key == OF_KEY_RIGHT) {
|
|
nextLinePressed();
|
|
}
|
|
|
|
if(key == OF_KEY_LEFT) {
|
|
pastLine();
|
|
}
|
|
|
|
if(key == 'r' || key == 'R'){
|
|
resetScript();
|
|
}
|
|
}
|
|
|
|
void ofTeleprompter::pastLine() {
|
|
if (currentLine < script.size()) {
|
|
currentLine--;
|
|
currentSpeaker = (*activeScript)[currentLine].speaker;
|
|
currentEmotion = (*activeScript)[currentLine].emotion;
|
|
currentSentence = (*activeScript)[currentLine].sentence;
|
|
displayedSentence.clear();
|
|
currentLetterIndex = 0;
|
|
lastWordTime = ofGetElapsedTimeMillis();
|
|
}
|
|
}
|