Compare commits

..

8 Commits

9 changed files with 170 additions and 500 deletions
Vendored
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+1 -323
View File
@@ -53,326 +53,4 @@ Make sure that **ffmpeg** is installed on your system, as it may be required for
---
**Installation complete!** You're now ready to use Video2Document.
###General Despription
## Project Overview
V2D (Video to Document Framework) is a student project developed to convert video and audio content into structured, readable documents using AI-based text processing. The tool is designed to help users automatically generate documents such as meeting reports, summaries or structured texts from recorded videos or audio files.
The main problem V2D addresses is the time-consuming manual effort required to listen to recordings and write documents afterwards. By automating transcription, structuring and document generation, the tool improves efficiency and usability for students, teams and small organizations.
The project was created as part of a university software engineering course and is developed using agile methods (Scrum). It focuses on modular design, usability and extensibility, allowing new document types and features to be added easily.
### Used Packages and Dependencies
This project uses several Node.js packages to handle video processing, AI-based document generation, backend communication, and the desktop user interface. Below is an overview of the most important dependencies and their purpose.
##Core Dependencies:
Electron:
Used to build the desktop application. It allows the project to run as a cross-platform GUI application using web technologies.
Express:
Acts as a lightweight backend server that handles internal API requests and communication between different parts of the application.
Axios:
Used for making HTTP requests, for example when communicating with external APIs such as AI services.
dotenv:
Used to securely load API keys and other sensitive configuration values from environment variables instead of hardcoding them in the source code.
@google/genai:
Provides access to Googles Generative AI models. This package is used to generate structured documents from transcribed text based on configurable prompts.
ffmpeg-static:
Provides a static FFmpeg binary so that video and audio processing works without requiring FFmpeg to be installed separately on the system.
fluent-ffmpeg:
Used together with FFmpeg to process video and audio files, for example extracting audio tracks from uploaded videos.
puppeteer:
Used to render and process HTML content programmatically. This is helpful for document previews and automated content generation.
html-to-docx:
Converts generated HTML documents into downloadable DOCX files, which are provided to the user as the final output.
mocha:
A testing framework used to run unit tests and ensure that core functionalities work as expected.
##Development Dependencies:
TypeScript:
Used to add static typing to the project, improving code quality, readability, and maintainability.
ts-node:
Allows running TypeScript files directly without manually compiling them first.
@types/node:
Provides TypeScript type definitions for Node.js APIs.
@types/fluent-ffmpeg:
Type definitions for fluent-ffmpeg to improve development experience and error checking.
@types/cli-progress:
Provides type support for progress bar functionality used during processing tasks.
###Why These Packages Are Needed
Together, these packages enable:
*Processing video and audio files
*Communicating with AI models for document generation
*Secure handling of API keys
*Generating structured documents (DOCX)
*Providing a user-friendly desktop interface
*Ensuring code quality through testing
###API Keys and Configuration
The V2D Video to Document tool uses external AI and media processing services to convert video and audio content into structured documents.
To access these services, several API keys are required. For security reasons, API keys are not stored in the repository and must be provided via environment variables.
##Supported API Keys
The project currently supports the following API keys. Depending on the configuration and selected provider, one or more of these keys may be used.
Google Gemini API
Environment variable: GOOGLE_API_KEY
Usage:
Used for AI-based document generation. The Large Language Model processes transcripts and creates structured documents such as meeting reports.
OpenAI (ChatGPT)
Environment variable: OPENAI_API_KEY
Usage:
Alternative AI provider for text processing and document generation.
AssemblyAI
Environment variable: ASSEMBLYAI_API_KEY
Usage:
Speech-to-text processing for audio and video files.
Saya
Environment variable: SAYA_API_KEY
Usage:
Additional or experimental AI provider that can be integrated into the document generation pipeline.
##How to Set API Keys
API keys must be configured as environment variables before starting the application.
Linux / macOS
export GOOGLE_API_KEY="your_api_key_here"
export OPENAI_API_KEY="your_api_key_here"
export ASSEMBLYAI_API_KEY="your_api_key_here"
export SAYA_API_KEY="your_api_key_here"
Windows (PowerShell)
setx GOOGLE_API_KEY "your_api_key_here"
setx OPENAI_API_KEY "your_api_key_here"
setx ASSEMBLYAI_API_KEY "your_api_key_here"
setx SAYA_API_KEY "your_api_key_here"
Alternatively, for local development, a .env file can be used:
GOOGLE_API_KEY=your_api_key_here
OPENAI_API_KEY=your_api_key_here
ASSEMBLYAI_API_KEY=your_api_key_here
SAYA_API_KEY=your_api_key_here
⚠️ Important:
The .env file must not be committed to the repository and should be listed in .gitignore.
Security Notes:
*API keys are injected at runtime
*No secrets are stored in the source code
*Prevents accidental exposure of sensitive data
*Supports secure collaboration in GitLab and CI/CD environments
*Follows best practices for secret management
###End-to-End User Guide (Video → Final Document)
This section describes how a user can create a structured document from a video using the V2D Video to Document tool.
Start the Application:
Ensure all required API keys are configured as environment variables.
Install dependencies:
npm install
Start the application:
npm start
The Electron-based GUI will open.
#Upload a Video File:
In the application interface, select Upload Video.
Choose a supported video file (e.g. .mp4, .mov).
The video is loaded into the system for processing.
#Audio Extraction:
The application automatically extracts audio from the uploaded video.
This is handled internally using FFmpeg.
No user interaction is required for this step.
#Speech-to-Text Transcription:
The extracted audio is sent to the speech-to-text service.
The transcription process converts spoken content into text.
The generated transcript is stored internally and used for further processing.
#Select Document Type:
The user selects a document type (e.g. Meeting Report).
Each document type is based on a predefined prompt template.
The selected template defines the structure and style of the final document.
#Document Generation:
The transcript and selected prompt are sent to the AI service.
The AI model processes the input and generates a structured document.
The output is formatted in Markdown.
#Document Preview:
The generated document is displayed in the application preview.
Users can review the content before exporting.
No manual editing is required, but validation is possible.
#Export the Final Document:
The user exports the document in the desired format.
#Supported formats include:
Markdown (.md)
Word (.docx)
The document is saved locally.
#Completion:
The final document is now ready for use.
The user can repeat the process with another video if needed.
###Resources
This section lists the main technologies, libraries, and external resources used in the V2D (Video to Document) project. These resources are required to understand, run, and further develop the application.
##Project Dependencies
The following packages and tools are used in this project (as defined in package.json):
Application & Backend
Node.js JavaScript runtime environment used for backend processing
Electron Framework for building the desktop application
Express Backend web framework for handling requests and internal APIs
AI & API Communication
@google/genai Used for AI-based document generation
Axios HTTP client for communicating with external APIs (LLMs)
Video & Audio Processing
ffmpeg-static Extracts audio from uploaded video files
fluent-ffmpeg Controls FFmpeg operations programmatically
Document Generation & Preview
html-to-docx Converts structured content into Word documents
Puppeteer Renders HTML content for document preview and processing
Configuration & Security
dotenv Loads API keys securely from environment variables
Testing & Development
Mocha Unit testing framework
TypeScript Improves code quality and type safety
These dependencies enable the complete end-to-end workflow from video input to structured document output.
##Relevant Repositories
V2D Main Repository:
https://gitlab.rlp.net/proj-wise2526-video2document/video2document
This repository contains the full source code, configuration files, and documentation for the V2D project.
Downloads & External Resources
The following tools and documentation may be required to run or understand the project:
Node.js: https://nodejs.org
Electron Documentation: https://www.electronjs.org/docs
FFmpeg: https://ffmpeg.org
Google Generative AI: https://ai.google.dev
Puppeteer: https://pptr.dev
**Installation complete!** You're now ready to use Video2Document.
+33 -120
View File
@@ -1,6 +1,5 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@@ -22,7 +21,7 @@
padding: 30px;
margin-top: 50px;
border-radius: 12px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
width: 90%;
max-width: 600px;
}
@@ -39,9 +38,7 @@
color: #555;
}
input[type="text"],
textarea,
select {
input[type="text"], textarea, select {
width: 100%;
padding: 10px;
margin-top: 5px;
@@ -61,10 +58,6 @@
margin-top: 25px;
}
.hidden {
visibility: hidden;
}
button {
padding: 10px 20px;
font-size: 14px;
@@ -84,7 +77,6 @@
.buttons {
flex-direction: column;
}
.buttons button {
width: 100%;
margin-top: 10px;
@@ -98,22 +90,21 @@
}
</style>
</head>
<body>
<div class="container">
<h1>Manage document types</h1>
<h1>Custom Document Generator</h1>
<label for="docName">Dokumentname:</label>
<input type="text" id="docName" placeholder="Gib hier den Dokumentnamen ein">
<label for="existingDocs">Vorhandene Dokumente auswählen (optional):</label>
<!--Drop Down-->
<select name="existingDocs" id="existingDocs">
<option value="newDoc">-- Neues Dokument erstellen --</option>
<select id="existingDocs">
<option value="">-- Neues Dokument erstellen --</option>
<option value="meeting_report_001">Meeting Report 001</option>
<option value="summary_01">Summary 01</option>
<option value="project_plan_A">Project Plan A</option>
</select>
<div id="docNameWrapper">
<label for="docName">Dokumentname:</label>
<input type="text" id="docName" placeholder="Gib hier den Dokumentnamen ein">
</div>
<label for="prompt">Dein Prompt:</label>
<textarea id="prompt" placeholder="Schreibe hier den Prompt für dein Dokument..."></textarea>
@@ -121,128 +112,50 @@
<a href="index.html">
<button id="goBackBtn">Abbrechen</button>
</a>
<button id="deleteBtn">Dokument löschen</button>
<button id="generateBtn">Dokument speichern</button>
</div>
<div id="result"></div>
</div>
<script src="languages.js"></script>
<script>
const goBackBtn = document.getElementById("goBackBtn");
const generateBtn = document.getElementById("generateBtn");
const deleteBtn = document.getElementById("deleteBtn");
const existingDocs = document.getElementById("existingDocs");
const docNameInput = document.getElementById("docName");
const promptInput = document.getElementById("prompt");
const resultDiv = document.getElementById("result");
const exampleText = "";
// dokumente speichern
// Zurück zur Haupt-GUI
goBackBtn.addEventListener("click", () => {
window.electronAPI.goBackToMain();
});
// Generiere Dokument
generateBtn.addEventListener("click", () => {
const name = docNameInput.value.trim();
const content = promptInput.value.trim();
if (!name || !content) {
resultDiv.textContent = "Bitte Dokumentname und Prompt ausfüllen.";
setTimeout(() => {
resultDiv.textContent = "";
}, 3000);
return;
}
window.api.saveTxtFile(name, content).then();
resultDiv.textContent = "Dokument erfolgreich gespeichert!";
setTimeout(() => {
resultDiv.textContent = "";
}, 3000);
reloadDocuments();
const prompt = promptInput.value.trim();
let docName = docNameInput.value.trim();
const selectedExisting = existingDocs.value;
});
// dokumente löschen
deleteBtn.addEventListener("click", () => {
const name = docNameInput.value.trim();
if (!name) {
resultDiv.textContent = "Bitte Dokumentname angeben.";
setTimeout(() => {
resultDiv.textContent = "";
}, 3000);
if (!prompt) {
alert("Bitte gib einen Prompt ein!");
return;
}
const confirmDelete = confirm(
`Möchtest du das Dokument "${name}" wirklich löschen?`
);
if (!confirmDelete) return;
window.api.deleteTxtFile(name).then((success) => {
if (success) {
resultDiv.textContent = "Dokument erfolgreich gelöscht!";
reloadDocuments();
existingDocs.value = "newDoc";
existingDocs.dispatchEvent(new Event("change"));
} else {
resultDiv.textContent = "Dokument konnte nicht gelöscht werden.";
}
setTimeout(() => {
resultDiv.textContent = "";
}, 3000);
});
});
//function to load existingDoc options to the drop down list
const select = document.getElementById('existingDocs');
window.api.getTxtFiles().then(files => {
reloadDocuments();
});
//content anzeigen
const docNameWrapper = document.getElementById("docNameWrapper");
existingDocs.addEventListener("change", async () => {
const selected = existingDocs.value;
if (selected === "newDoc") {
docNameWrapper.classList.remove("hidden");
docNameInput.value = "";
promptInput.value = exampleText;
// Wenn ein vorhandenes Dokument ausgewählt wurde, hängt der Prompt daran
if (selectedExisting) {
docName = selectedExisting; // prompt wird an vorhandenes Dokument angehängt
} else if (!docName) {
alert("Bitte gib einen Dokumentnamen ein, wenn du ein neues Dokument erstellen möchtest!");
return;
}
docNameWrapper.classList.add("hidden");
const content = await window.api.readTxtFile(selected);
promptInput.value = content;
docNameInput.value = selected.replace(".txt", "");
// Demo-Ausgabe im Result-Div
resultDiv.innerHTML = `<strong>Dokumentname:</strong> ${docName}<br><strong>Prompt:</strong> ${prompt}`;
// Hier kannst du den Prompt an dein LLM oder Module-Handler senden
// z.B. window.submit.submit({documentName: docName, prompt: prompt})
});
//reload drop down
function reloadDocuments() {
[...existingDocs.querySelectorAll('option:not([value="newDoc"])')]
.forEach(o => o.remove());
window.api.getTxtFiles().then(files => {
files.forEach(file => {
const option = document.createElement('option');
option.value = file;
option.textContent = file
.replace('.txt', '') // Endung entfernen
.replace(/_/g, ' ') // Leerzeichen ersetzen
.replace(/\b\w/g, c => c.toUpperCase()) // ersten Buchstaben groß
existingDocs.appendChild(option);
//customDocumentTypes.appendChild(option);
});
});
}
</script>
<script src="./renderer.js"></script>
</body>
</html>
</html>
+4 -3
View File
@@ -19,8 +19,8 @@
</label>
<nav class="menu1">
<a href="custom_document.html" class="li1">Manage document types</a>
<a href="help_page.html" class="li1">Help</a>
<a href="custom_document.html" class="li1">Custom document</a>
<a href="" class="li1">Help</a>
</nav>
</nav>
</section>
@@ -105,7 +105,8 @@
</div>
<div class="checkbox-container">
<input type="checkbox" name="docFormat" id="docFormatCustom" value="custom">
<select name="customDocumentTypes" id="customDocumentTypes">
<select name="ai_type" id="ai_type">
<option>nichts</option>
</select>
</div>
</div>
+6 -17
View File
@@ -5,10 +5,10 @@ try {
onFileDrop: (file) => webUtils.getPathForFile(file)
})
contextBridge.exposeInMainWorld("submit", {
submit: (meeting_specifications) => { ipcRenderer.send("file_submit", meeting_specifications) }
submit: (meeting_specifications) => {ipcRenderer.send("file_submit", meeting_specifications)}
})
contextBridge.exposeInMainWorld("electronAPI", {
getFilePath: (file) => { return webUtils.getPathForFile(file) }
getFilePath: (file) => {return webUtils.getPathForFile(file)}
})
contextBridge.exposeInMainWorld("onStartup", {
@@ -23,26 +23,15 @@ try {
speakerAudios: (callback) => ipcRenderer.on('speakerAudios', callback)
})
contextBridge.exposeInMainWorld("submitSpeaker", {
speaker_submit: (speaker_names) => { ipcRenderer.send("speaker_submit", speaker_names) }
speaker_submit: (speaker_names) => {ipcRenderer.send("speaker_submit", speaker_names)}
})
contextBridge.exposeInMainWorld("download", {
file_download: () => { ipcRenderer.send("file_download") }
file_download: () => {ipcRenderer.send("file_download")}
})
//documenttypes
contextBridge.exposeInMainWorld('api', {
getTxtFiles: () => ipcRenderer.invoke('get-txt-files'),
saveTxtFile: (name, content) =>
ipcRenderer.invoke('save-txt-file', name, content),
readTxtFile: (fileName) =>
ipcRenderer.invoke('read-txt-file', fileName),
deleteTxtFile: (fileName) =>
ipcRenderer.invoke('delete-txt-file', fileName)
});
ipcRenderer.on("error", (event, err) => { alert(err) })
ipcRenderer.on("error", (event, err) => {alert(err)})
} catch (error) {
console.log("Error in preload.js");
}
+1 -37
View File
@@ -262,40 +262,4 @@ let q1 = {
{name:"abc", displayname:"ABC"},
{name:"qeg", displayname:"aqghegahu"}
]
}
//gibt Documentfiles an preload zurück
electron.ipcMain.handle('get-txt-files', () => {
const storagePath = `${mainDir}/storage/documentType`
return fs.readdirSync(storagePath)
.filter(f => f.endsWith('.txt'))
});
//speichern neuer document types
electron.ipcMain.handle('save-txt-file', (event, fileName, content) => {
const filePath = `${mainDir}/storage/documentType/${fileName}.txt`;
fs.writeFileSync(filePath, content, 'utf8');
return true;
});
//read file content
electron.ipcMain.handle('read-txt-file', (event, fileName) => {
const filePath = `${mainDir}/storage/documentType/${fileName}`;
return fs.readFileSync(filePath, 'utf8');
});
//delete documentfiles
electron.ipcMain.handle('delete-txt-file', (event, fileName) => {
const filePath = `${mainDir}/storage/documentType/${fileName}.txt`;
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
return true;
} else {
return false;
}
});
}
@@ -0,0 +1,54 @@
// -----------------------------------------------------------
// Parakeet (Step 3A: spawn Python minimal integration)
// -----------------------------------------------------------
const fs = require("fs");
const path = require("path");
const { spawn } = require("child_process");
module.exports = {
name: "parakeet",
type: "transcription",
displayname: "NVIDIA Parakeet",
async function(audioFilePath) {
console.log("🦜 [Parakeet] Starting test integration (spawn only)...");
console.log("🦜 Input audio:", audioFilePath);
// Check audio exists
if (!fs.existsSync(audioFilePath)) {
throw new Error("Audio file does not exist: " + audioFilePath);
}
// Output path in storage/transcripts
const sessionId = path.basename(audioFilePath).replace(/\.[^.]+$/, "");
const outputDir = path.join(__dirname, "../../../storage/transcripts");
fs.mkdirSync(outputDir, { recursive: true });
const outputPath = path.join(outputDir, `${sessionId}.json`);
// -------------------------------------------------------
// SPAWN PYTHON SCRIPT (step 3A — dummy script)
// -------------------------------------------------------
return new Promise((resolve, reject) => {
const python310 = "C:\\Users\\smith\\AppData\\Local\\Programs\\Python\\Python310\\python.exe";
const py = spawn(python310, [
path.join(__dirname, "parakeet_transcribe.py"),
audioFilePath,
outputPath
]);
py.stdout.on("data", data => console.log("🦜 [Python]", data.toString().trim()));
py.stderr.on("data", data => console.error("🦜 [Python ERR]", data.toString().trim()));
py.on("close", code => {
if (code === 0) {
console.log("🦜 [Parakeet] Done (spawn test). Output:", outputPath);
resolve(outputPath);
} else {
reject(new Error("Python script failed with exit code " + code));
}
});
});
}
};
@@ -0,0 +1,71 @@
# -----------------------------------------------------------
# Parakeet Real Transcriber (NVIDIA NeMo + PyTorch GPU)
# -----------------------------------------------------------
import sys
import json
import soundfile as sf
import torch
from nemo.collections.asr.models import ASRModel
# Args:
# sys.argv[1] = input audio path
# sys.argv[2] = output JSON path
audio_path = sys.argv[1]
output_path = sys.argv[2]
print("🔥 Starting Parakeet model...")
device = "cuda" if torch.cuda.is_available() else "cpu"
print("🔥 Using device:", device)
# -----------------------------------------------------------
# Load Parakeet model (NVIDIA pretrained ASR)
# -----------------------------------------------------------
model = ASRModel.from_pretrained(model_name="nvidia/parakeet-ctc-0.6b")
model = model.to(device)
model.eval()
# -----------------------------------------------------------
# Load audio
# -----------------------------------------------------------
print("🎧 Loading audio:", audio_path)
audio, sr = sf.read(audio_path)
# model expects mono float32
if len(audio.shape) > 1:
audio = audio.mean(axis=1)
audio = audio.astype("float32")
# -----------------------------------------------------------
# Run inference
# -----------------------------------------------------------
print("🧠 Running inference...")
with torch.no_grad():
hyp = model.transcribe([audio])[0]
# Extract only the text
if hasattr(hyp, "text"):
transcript = hyp.text
else:
# fallback: convert to string (rare)
transcript = str(hyp)
print("📄 Transcript:", transcript)
# -----------------------------------------------------------
# Save JSON format compatible with V2D pipeline
# -----------------------------------------------------------
result = {
"id": output_path.split("/")[-1].replace(".json", ""),
"tool": "nemo_parakeet",
"status": "completed",
"text": transcript,
"words": [] # Parakeet XS doesnt return word timestamps
}
with open(output_path, "w", encoding="utf-8") as f:
json.dump(result, f, indent=2, ensure_ascii=False)
print("✔ JSON saved at:", output_path)