#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 & _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(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 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(); } }