Compare commits

...

29 Commits

Author SHA1 Message Date
Azeufack Noupeu Willy 84ab93ce77 chore: update package-lock.json after axios installation 2025-11-24 19:53:33 +01:00
Azeufack Noupeu Willy 173ed90642 feat(S3-06): integrate orchestrator in main.js and complete assembly module 2025-11-24 19:36:50 +01:00
Azeufack Noupeu Willy 925eb33eab chore: add .env.example template for AssemblyAI API key 2025-11-20 15:42:03 +01:00
Azeufack Noupeu Willy 911cba14fd chore(typescript): add @ts-ignore for assembly module import 2025-11-20 14:34:07 +01:00
Azeufack Noupeu Willy 6813659443 feat(main): add pipeline orchestrator for auto-transcription 2025-11-20 14:31:26 +01:00
Azeufack Noupeu Willy a0ed2ab7bd feat(extraction): add audio_ready event emission
- Add EventEmitter to emit audio_ready when extraction completes
- Pass sessionId and audioPath in event data
- Export audioEvents for Main process orchestrator

Refs: S3-06 AC1,AC3,AC7
2025-11-20 14:05:52 +01:00
Hughes, Mike 4b72568ad3 Merge branch 'feature/ui-test' into 'develop'
Implemented a dropdown field for the languages as well as functions and a file...

See merge request proj-wise2526-video2document/video2document!23
2025-11-16 14:52:29 +01:00
MikeHughes-BIN 76c18fa713 Merge branch 'develop' into feature/ui-test 2025-11-15 15:46:26 +01:00
Spanier, Pit 2edc7f8351 Merge branch 'fix/transcription-module-fix' into 'develop'
New Folder structure

See merge request proj-wise2526-video2document/video2document!21
2025-11-15 15:11:35 +01:00
MikeHughes-BIN 6083773f88 New Folder structure 2025-11-15 14:45:13 +01:00
eric.minning 4a91f03289 Implemented a dropdown field for the languages as well as functions and a file to easily implement other languages. 2025-11-14 18:32:41 +01:00
Spanier, Pit 444d408480 Merge branch 'feature/fixing-the-program' into 'develop'
fixed the program by moving the example module back to where the program can ACTUALLY load it

See merge request proj-wise2526-video2document/video2document!20
2025-11-14 14:30:16 +01:00
emily d9eacafc3a fixed the program by moving the example module back to where the program can ACTUALLY load it 2025-11-14 14:28:11 +01:00
Hughes, Mike ab737f0dc9 Merge branch 'feature/12-externe-transkription-s2-02b' into 'develop'
feat(S2-02b): Implement AssemblyAI external transcription with speaker diarization

See merge request proj-wise2526-video2document/video2document!18
2025-11-13 17:38:39 +01:00
MikeHughes-BIN 79e0c48755 Reduced Number of test paths to avoid redundancy 2025-11-13 17:35:40 +01:00
MikeHughes-BIN 9254ddc57f Changed the Folder Structure for better maintainability 2025-11-13 17:34:22 +01:00
Azeufack Noupeu Willy c021272ca4 merge: Resolve conflicts with develop 2025-11-13 15:22:33 +01:00
Azeufack Noupeu Willy e7e97a7f60 feat(S2-02b): Implement AssemblyAI external transcription with speaker diarization
- Add assembly.ts module for REST API transcription via AssemblyAI
- Implement 5-step pipeline: upload → create job → poll status → download → save
- Enable speaker_labels for diarization (Speaker A, B, C...)
- Add millisecond-precision timestamps for each utterance
- Store JSON transcripts in storage/transcripts/{session_id}.json
- Add axios, dotenv dependencies
- Add transcribeLatest.ts helper for quick testing

User Story: S2-02b - Externe Transkription per REST API
2025-11-13 13:07:18 +01:00
Spanier, Pit 80392874bb Merge branch 'feature/combination-of-all-the-things' into 'develop'
Fixed an error in checkBoxes function and uploadContainer "drop" listener...

See merge request proj-wise2526-video2document/video2document!15
2025-11-12 20:56:11 +01:00
eric.minning 1a681eb2b8 Improved error log and implemented a variable and function for file path storage 2025-11-12 20:14:56 +01:00
Aarthi Manivannan, Premanathan Aarthi Manivannan af794e0245 Merge branch 'feature/ci-pipeline-s1-09a-1' into 'develop'
Feature/ci pipeline s1 09a 1

See merge request proj-wise2526-video2document/video2document!7
2025-11-10 18:07:55 +01:00
Aarthi Manivannan, Premanathan Aarthi Manivannan fb173a4041 Update .gitlab-ci.yml file 2025-11-06 16:21:01 +01:00
Aarthi Manivannan, Premanathan Aarthi Manivannan b70680e950 Ci file 2025-11-06 16:00:28 +01:00
Aarthi Manivannan, Premanathan Aarthi Manivannan a069452f87 requirements 2025-11-06 15:53:38 +01:00
Aarthi Manivannan, Premanathan Aarthi Manivannan fd4d342eeb Delete .gitlab-ci.yml 2025-11-06 15:52:45 +01:00
Aarthi Manivannan, Premanathan Aarthi Manivannan 70221683c3 tesing 2025-11-06 15:51:56 +01:00
Aarthi Manivannan, Premanathan Aarthi Manivannan a92e33fa59 Add new directory 2025-11-06 15:51:08 +01:00
Aarthi Manivannan, Premanathan Aarthi Manivannan 0dc2ebe99c App 2025-11-06 15:50:10 +01:00
Aarthi Manivannan, Premanathan Aarthi Manivannan a0fe1a80e3 Add new directory 2025-11-06 15:49:27 +01:00
31 changed files with 780 additions and 114 deletions
BIN
View File
Binary file not shown.
+33 -6
View File
@@ -1,6 +1,33 @@
build-job: workflow:
script: rules:
- echo "Building the Project.." # Run the pipeline for merge requests or when committing to a branch
test-job: - if: $CI_PIPELINE_SOURCE == "merge_request_event"
script: - if: $CI_COMMIT_BRANCH
- echo "Running Tests.."
image: python:3.14.0
stages:
- setup
- test
setup_environment:
stage: setup
script:
- pip install --upgrade pip
- pip install -r requirements.txt
- echo "Dependencies installed successfully."
only:
- main
- feature/ci-pipeline-s1-09a-1 # You can add more branches if needed
test_app:
stage: test
script:
- echo "Running V2D Framework basic test..."
- python -m unittest discover || echo "No tests found."
only:
- main
- feature/ci-pipeline-s1-09a-1
+7
View File
@@ -0,0 +1,7 @@
from fastapi import FastAPI
app = FastAPI()
@app.get("/health")
def health_check():
return {"status": "ok"}
+7 -5
View File
@@ -10,9 +10,10 @@
<div class="mitte" id="mitte"> <div class="mitte" id="mitte">
<div class="flagsBtns" id="flagsBtns"> <div class="flagsBtns" id="flagsBtns">
<button class="de_Btn" id="de_Btn" onclick="changeLanguage('de')"><img src="flags/germany-flag-png-large.jpg" width="25px" height="20px"/></button> <select name="ai_type" id="ai_type">
<button class="eng_Btn" id="eng_Btn" onclick="changeLanguage('en')"><img src="flags/united-kingdom-flag-png-large.jpg" width="25px" height="20px"/></button> </select>
<button class="in_Btn" id="in_Btn" onclick="changeLanguage('in')"><img src="flags/india-flag-png-large.png" width="25px" height="20px"/></button> <select name="language_option" id="language_option">
</select>
</div> </div>
<h1 id="h1">Video to document</h1> <h1 id="h1">Video to document</h1>
@@ -46,7 +47,8 @@
</div> </div>
</div> </div>
<script src="script.js"></script> <script src="languages.js"></script>
<script src="./renderer.js"></script> <script src="script.js"></script>
<script src="./renderer.js"></script>
</body> </body>
</html> </html>
+36
View File
@@ -0,0 +1,36 @@
var languageOptions = {
"eng":{
"title": "Video to document",
"h1": "Video to document",
"p1": "Drag and drop video file",
"fileName": "No video chosen",
"manualUploadBtn": "Search video",
"checkbox_group": "Choose prefered document style:",
"label_format": "Meeting report",
"label_summary": "Summary with timestamps",
"submitButton": "Submit"
},
"de":{
"title": "Video zu Dokument",
"h1": "Video zu Dokument",
"p1": "Video per Drag & Drop ablegen",
"fileName": "Kein Video ausgewaehlt",
"manualUploadBtn": "Video suchen",
"checkbox_group": "Bevorzugte Dokumentvarianten:",
"label_format": "Meeting Bericht",
"label_summary": "Zusammenfassung mit Zeitstempeln",
"submitButton": "Absenden"
},
"in":{
"title": "दस्तावेज़ के लिए वीडियो",
"h1": "दस्तावेज़ के लिए वीडियो",
"p1": "वीडियो फ़ाइल खींचें और छोड़ें",
"fileName": "कोई वीडियो नहीं चुना गया",
"manualUploadBtn": "वीडियो खोजें",
"checkbox_group": "पसंदीदा दस्तावेज़ शैली चुनें:",
"label_format": "बैठक रिपोर्ट",
"label_summary": "टाइमस्टैम्प के साथ सारांश",
"submitButton": "जमा करना"
}
};
+58 -1
View File
@@ -2,6 +2,9 @@ import { app, BrowserWindow, ipcMain, dialog } from 'electron';
import { exec } from 'child_process'; import { exec } from 'child_process';
import path from 'path'; import path from 'path';
import { fileURLToPath } from 'url'; import { fileURLToPath } from 'url';
// Import audio events and transcription module
import { audioEvents } from '../../services/modules/extraction/ffmpegExtractor.js';
import { transcribe } from '../../services/modules/transcription-remote/assembly.js';
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
@@ -22,7 +25,61 @@ function createWindow() {
mainWindow.loadFile('main/index.html'); mainWindow.loadFile('main/index.html');
} }
app.whenReady().then(createWindow); // Setup pipeline orchestrator
function setupOrchestrator() {
console.log('🎯 [Pipeline] Orchestrator ready. Listening for audio_ready events...');
audioEvents.on('audio_ready', async (data) => {
const { audioPath, sessionId } = data;
console.log(`✅ [Pipeline] Audio ready: ${sessionId}`);
// AC6: Send status to UI - Audio bereit
mainWindow.webContents.send('pipeline-status', {
sessionId,
status: 'audio_ready',
message: 'Audio bereit'
});
try {
// AC4: Status transcription_started
console.log(`🚀 [Pipeline] Starting transcription: ${sessionId}`);
mainWindow.webContents.send('pipeline-status', {
sessionId,
status: 'transcription_started',
message: 'Transkription gestartet'
});
// AC2: Auto-start transcription (S2-02b)
await transcribe(audioPath, sessionId);
// AC6: Status transcription_completed
console.log(`✅ [Pipeline] Transcription completed: ${sessionId}`);
mainWindow.webContents.send('pipeline-status', {
sessionId,
status: 'transcription_completed',
message: 'Transkription abgeschlossen'
});
} catch (error) {
// AC5: Error logging + failed_transcription_start
console.error(`❌ [Pipeline] Transcription failed: ${sessionId}`);
console.error(` Error:`, error.message);
mainWindow.webContents.send('pipeline-status', {
sessionId,
status: 'failed_transcription_start',
message: 'Fehler beim Transkriptionsstart',
error: error.message
});
}
});
}
app.whenReady().then(() => {
createWindow();
setupOrchestrator();
});
// Kommunikation vom Renderer (Frontend) // Kommunikation vom Renderer (Frontend)
ipcMain.handle('convert-video', async (event, filePath) => { ipcMain.handle('convert-video', async (event, filePath) => {
+1 -4
View File
@@ -1,5 +1,3 @@
const { contextBridge, ipcRenderer, webUtils } = require('electron') const { contextBridge, ipcRenderer, webUtils } = require('electron')
try { try {
@@ -14,5 +12,4 @@ try {
}) })
} catch (error) { } catch (error) {
console.log("Error in preload.js"); console.log("Error in preload.js");
} }
+14 -4
View File
@@ -25,7 +25,17 @@ uploadContainer.addEventListener("drop", (e) => {
} }
} catch (error) { } catch (error) {
console.log("Error in renderer.js with the listerner for the drop function"); console.log("Error in renderer.js with the listerner for the drop function");
} }
})
})
window.addEventListener('load', (e) => {
console.log("test");
loadLanguageOptions();
});
language_option.addEventListener('change', (e)=>{
const select = document.getElementById('language_option');
console.log(select.value);
changeLanguage(select.value);
});
+52 -35
View File
@@ -1,4 +1,3 @@
//listener for the file explorer search //listener for the file explorer search
manualUploadBtn.addEventListener('click', () => { manualUploadBtn.addEventListener('click', () => {
try { try {
@@ -40,40 +39,22 @@ function checkBoxes() {
//language changing feature //language changing feature
function changeLanguage(language) { function changeLanguage(language) {
if (language === 'en') { try {
document.getElementById('title').textContent = 'Video to document'; document.getElementById('title').textContent = languageOptions[language].title;
document.getElementById('h1').textContent = 'Video to document'; document.getElementById('h1').textContent = languageOptions[language].h1;
document.getElementById('p1').textContent = 'Drag and drop video file'; document.getElementById('p1').textContent = languageOptions[language].p1;
document.getElementById('fileName').textContent = 'No video chosen'; document.getElementById('fileName').textContent = languageOptions[language].fileName;
document.getElementById('manualUploadBtn').textContent = 'Search video'; document.getElementById('manualUploadBtn').textContent = languageOptions[language].manualUploadBtn;
document.getElementById('checkbox_group').textContent = 'Choose prefered document style:'; document.getElementById('checkbox_group').textContent = languageOptions[language].checkbox_group;
document.getElementById('label_format').textContent = 'Meeting report'; document.getElementById('label_format').textContent = languageOptions[language].label_format;
document.getElementById('label_summary').textContent = 'Summary with timestamps'; document.getElementById('label_summary').textContent = languageOptions[language].label_summary;
document.getElementById('submitButton').textContent = 'Submit'; document.getElementById('submitButton').textContent = languageOptions[language].submitButton;
} else if (language === 'de') { } catch (error) {
document.getElementById('title').textContent = 'Video zu Dokument'; console.log(error);
document.getElementById('h1').textContent = 'Video zu Dokument'; }
document.getElementById('p1').textContent = 'Video per Drag & Drop ablegen';
document.getElementById('fileName').textContent = 'Kein Video ausgewaehlt';
document.getElementById('manualUploadBtn').textContent = 'Video suchen';
document.getElementById('checkbox_group').textContent = 'Bevorzugte Dokumentvarianten:';
document.getElementById('label_format').textContent = 'Meeting Bericht';
document.getElementById('label_summary').textContent = 'Zusammenfassung mit Zeitstempeln';
document.getElementById('submitButton').textContent = 'Absenden';
} else if(language == "in") {
document.getElementById('title').textContent = 'दस्तावेज़ के लिए वीडियो';
document.getElementById('h1').textContent = 'दस्तावेज़ के लिए वीडियो';
document.getElementById('p1').textContent = 'वीडियो फ़ाइल खींचें और छोड़ें';
document.getElementById('fileName').textContent = 'कोई वीडियो नहीं चुना गया';
document.getElementById('manualUploadBtn').textContent = 'वीडियो खोजें';
document.getElementById('checkbox_group').textContent = 'पसंदीदा दस्तावेज़ शैली चुनें:';
document.getElementById('label_format').textContent = 'बैठक रिपोर्ट';
document.getElementById('label_summary').textContent = 'टाइमस्टैम्प के साथ सारांश';
document.getElementById('submitButton').textContent = 'जमा करना';
}
} }
//listener for the file explorer search when something got selected //listener for the file explorer search when something got selected
videoUpload.addEventListener('change', () => { videoUpload.addEventListener('change', () => {
try { try {
@@ -84,8 +65,6 @@ videoUpload.addEventListener('change', () => {
}); });
//function to display the file path in the drop down box //function to display the file path in the drop down box
function handleFiles(files) { function handleFiles(files) {
try { try {
@@ -112,4 +91,42 @@ function updateProgressBar(bar, value){
console.log("Error in scripts.js updateProgressBar function"); console.log("Error in scripts.js updateProgressBar function");
} }
}
//function to load ai options to the drop down list
function loadAiOptions(options){
try {
var menu = document.getElementById('ai_type');
for(i = 0; i < options.length; i++){
var opty = options[i];
var namey = "option" + i;
var choice = document.createElement(namey);
choice.textContent = "t";
choice.value = i;
menu.appendChild(choice);
}
} catch (error) {
console.log("Error in script.js loadAiOptions function");
console.log(error);
}
}
//function to load language options to the drop down list
function loadLanguageOptions(){
try {
var menu = document.getElementById('language_option');
var object_holdy;
var choice ;
object_holdy = Object.keys(languageOptions);
for(i = 0; i < object_holdy.length; i++){
choice = document.createElement('option');
choice.textContent = object_holdy[i];
choice.value = object_holdy[i];
menu.appendChild(choice);
}
} catch (error) {
console.log("Error in script.js loadLanguageOptions function");
console.log(error);
}
} }
+67
View File
@@ -33,6 +33,73 @@ console.log(`${mapFunctions.size} Function modules loaded`);
console.log("--------------------------------------------------------------------------------"); console.log("--------------------------------------------------------------------------------");
// ======================== S3-06 : PIPELINE ORCHESTRATOR ========================
// Get audioEvents from ffmpegExtractor module
const ffmpegExtractor = mapFunctions.get("extraction-video-to-audio");
const audioEvents = ffmpegExtractor.audioEvents;
console.log('🎯 [S3-06] Pipeline Orchestrator ready. Listening for audio_ready events...');
audioEvents.on('audio_ready', async (data) => {
const { audioPath, sessionId } = data;
console.log(`✅ [Pipeline] Audio ready: ${sessionId}`);
console.log(`📁 Audio path: ${audioPath}`);
// Send status to UI
if (mainWindow) {
mainWindow.webContents.send('pipeline-status', {
sessionId,
status: 'audio_ready',
message: 'Audio bereit'
});
}
try {
console.log(`🚀 [Pipeline] Starting transcription: ${sessionId}`);
if (mainWindow) {
mainWindow.webContents.send('pipeline-status', {
sessionId,
status: 'transcription_started',
message: 'Transkription gestartet'
});
}
// Get transcription module
const assemblyModule = mapFunctions.get("assembly");
if (assemblyModule && assemblyModule.run) {
await assemblyModule.run(audioPath);
} else {
console.warn('⚠️ Assembly module not found or missing run function');
}
console.log(`✅ [Pipeline] Transcription completed: ${sessionId}`);
if (mainWindow) {
mainWindow.webContents.send('pipeline-status', {
sessionId,
status: 'transcription_completed',
message: 'Transkription abgeschlossen'
});
}
} catch (error) {
console.error(`❌ [Pipeline] Transcription failed: ${sessionId}`);
console.error(` Error:`, error.message);
if (mainWindow) {
mainWindow.webContents.send('pipeline-status', {
sessionId,
status: 'failed_transcription_start',
message: 'Fehler beim Transkriptionsstart',
error: error.message
});
}
}
});
// ===============================================================================
// --------------------------------------------------------- CLI COMMANDS --------------------------------------------------------- // // --------------------------------------------------------- CLI COMMANDS --------------------------------------------------------- //
+152
View File
@@ -9,7 +9,10 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@types/axios": "^0.9.36",
"axios": "^1.13.2",
"cli-progress": "^3.12.0", "cli-progress": "^3.12.0",
"dotenv": "^17.2.3",
"electron": "^39.1.1", "electron": "^39.1.1",
"express": "^5.1.0", "express": "^5.1.0",
"ffmpeg-static": "^5.2.0", "ffmpeg-static": "^5.2.0",
@@ -149,6 +152,12 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/axios": {
"version": "0.9.36",
"resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.9.36.tgz",
"integrity": "sha512-NLOpedx9o+rxo/X5ChbdiX6mS1atE4WHmEEIcR9NLenRVa5HoVjAvjafwU3FPTqnZEstpoqCaW7fagqSoTDNeg==",
"license": "MIT"
},
"node_modules/@types/cacheable-request": { "node_modules/@types/cacheable-request": {
"version": "6.0.3", "version": "6.0.3",
"resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz",
@@ -290,6 +299,23 @@
"resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz", "resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
"integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ==" "integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ=="
}, },
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/axios": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/body-parser": { "node_modules/body-parser": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
@@ -419,6 +445,18 @@
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/concat-stream": { "node_modules/concat-stream": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
@@ -560,6 +598,15 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/depd": { "node_modules/depd": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -584,6 +631,18 @@
"node": ">=0.3.1" "node": ">=0.3.1"
} }
}, },
"node_modules/dotenv": {
"version": "17.2.3",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
"integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/dunder-proto": { "node_modules/dunder-proto": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -690,6 +749,21 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es6-error": { "node_modules/es6-error": {
"version": "4.1.1", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
@@ -834,6 +908,63 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/form-data/node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/form-data/node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/forwarded": { "node_modules/forwarded": {
"version": "0.2.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -1028,6 +1159,21 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": { "node_modules/hasown": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -1380,6 +1526,12 @@
"node": ">= 0.10" "node": ">= 0.10"
} }
}, },
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/pump": { "node_modules/pump": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
+3
View File
@@ -1,6 +1,9 @@
{ {
"dependencies": { "dependencies": {
"@types/axios": "^0.9.36",
"axios": "^1.13.2",
"cli-progress": "^3.12.0", "cli-progress": "^3.12.0",
"dotenv": "^17.2.3",
"electron": "^39.1.1", "electron": "^39.1.1",
"express": "^5.1.0", "express": "^5.1.0",
"ffmpeg-static": "^5.2.0", "ffmpeg-static": "^5.2.0",
+3
View File
@@ -0,0 +1,3 @@
fastapi
uvicorn
pytest
-5
View File
@@ -1,5 +0,0 @@
npx ts-node ./extract.ts /Users/mikehughes/Downloads/Testvideo/Kurzgesagt.mov
npx ts-node ./transcribe.ts ../storage/audio/Kurzgesagt.wav
npx ts-node ./extract.ts /Users/mikehughes/Downloads/Testvideo/GitLabMeeting.mov
npx ts-node ./transcribe.ts ../storage/audio/GitLabMeeting.wav
-23
View File
@@ -1,23 +0,0 @@
#!/usr/bin/env ts-node
import { extractAudioFromVideo } from "../services/modules/extraction/ffmpegExtractor.ts";
const videoPath = process.argv[2];
if (!videoPath) {
console.error("Usage: ts-node extractAudio.ts <videoPath>");
process.exit(1);
}
(async () => {
try {
console.log(`Extracting audio from: ${videoPath}`);
await extractAudioFromVideo(videoPath); // Call the extraction function (ffmpegExtractor.ts in services/modules/extraction)
console.log("Audio extraction completed successfully.");
} catch (err) {
console.error("Audio extraction failed:", err);
process.exit(1);
}
})();
-18
View File
@@ -1,18 +0,0 @@
import { whisperLocal } from "../services/modules/transcription/local/whisperLocal.ts";
const audioPath = process.argv[2];
if (!audioPath) {
console.error("Please provide an audio file path as argument.");
process.exit(1);
}
const whisper = new whisperLocal();
(async () => {
try {
const text = await whisper.transcribe(audioPath);
console.log(text);
} catch (err) {
console.error("Transcription failed:", err);
}
})();
+10 -2
View File
@@ -1,3 +1,5 @@
const EventEmitter = require('events');
const audioEvents = new EventEmitter();
// Ensure ffmpeg binary is available // Ensure ffmpeg binary is available
if (!ffmpegPath) { if (!ffmpegPath) {
@@ -75,6 +77,12 @@ module.exports = {
progressBar.update(100, { timemark: 'done' }); progressBar.update(100, { timemark: 'done' });
progressBar.stop(); progressBar.stop();
console.log(`Extraction completed: ${outputAudioPath}`); console.log(`Extraction completed: ${outputAudioPath}`);
audioEvents.emit('audio_ready', {
sessionId: inputVideoName,
audioPath: outputAudioPath
});
resolve(); resolve();
}) })
.on('error', (err) => { .on('error', (err) => {
@@ -88,6 +96,6 @@ module.exports = {
console.log(); console.log();
} }
}); });
} },
audioEvents: audioEvents
} }
@@ -5,7 +5,7 @@ import { fileURLToPath } from "url"; // To handle __dirname in ES modules
const __filename = fileURLToPath(import.meta.url); // Get current file path const __filename = fileURLToPath(import.meta.url); // Get current file path
const __dirname = path.dirname(__filename); // Get current directory path const __dirname = path.dirname(__filename); // Get current directory path
const transcriptsDir = path.resolve(__dirname, "../../storage/transcriptions"); const transcriptsDir = path.resolve(__dirname, "../../../storage/transcriptions");
export class whisperLocal { // is called by transcribe.ts export class whisperLocal { // is called by transcribe.ts
@@ -26,7 +26,6 @@ export class whisperLocal { // is called by transcribe.ts
async transcribe(audioPath: string): Promise<string> { //asyncronous function to transcribe audio async transcribe(audioPath: string): Promise<string> { //asyncronous function to transcribe audio
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const transcriptsDir = path.resolve(__dirname, "../../../../storage/transcripts"); //storage directory for transcripts
if (!fs.existsSync(transcriptsDir)) { //if transcripts directory does not exist, create it if (!fs.existsSync(transcriptsDir)) { //if transcripts directory does not exist, create it
fs.mkdirSync(transcriptsDir, { recursive: true }); fs.mkdirSync(transcriptsDir, { recursive: true });
@@ -0,0 +1,128 @@
require('dotenv/config');
const axios = require('axios');
const fs = require('fs');
const path = require('path');
const API_KEY = process.env.ASSEMBLYAI_API_KEY;
const BASE_URL = 'https://api.assemblyai.com/v2';
/**
* Uploads audio file to AssemblyAI
*/
async function uploadAudio(audioPath) {
const audioData = fs.readFileSync(audioPath);
const response = await axios.post(`${BASE_URL}/upload`, audioData, {
headers: {
'authorization': API_KEY,
'content-type': 'application/octet-stream'
}
});
return response.data.upload_url;
}
/**
* Extract session id from path or URL
*/
function getSessionId(inputPath) {
try {
const parsed = new URL(inputPath);
const base = path.basename(parsed.pathname);
return base.replace(/\.[^.]+$/, '');
} catch (err) {
return path.basename(inputPath, path.extname(inputPath));
}
}
/**
* Creates transcription job with speaker diarization
*/
async function createTranscript(audioUrl) {
const response = await axios.post(`${BASE_URL}/transcript`, {
audio_url: audioUrl,
speaker_labels: true,
language_detection: true
}, {
headers: {
'authorization': API_KEY,
'content-type': 'application/json'
}
});
return response.data.id;
}
/**
* Polls transcript status until completed
*/
async function pollTranscript(transcriptId) {
while (true) {
const response = await axios.get(`${BASE_URL}/transcript/${transcriptId}`, {
headers: { 'authorization': API_KEY }
});
const status = response.data.status;
if (status === 'completed') {
return response.data;
} else if (status === 'error') {
throw new Error(`Transcription failed: ${response.data.error}`);
}
// Wait 3 seconds before next poll
await new Promise(resolve => setTimeout(resolve, 3000));
}
}
/**
* Saves transcript to storage
*/
function saveTranscript(transcript, sessionId) {
const outputDir = path.join(__dirname, '..', '..', '..', 'storage', 'transcripts');
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
const outputPath = path.join(outputDir, `${sessionId}.json`);
fs.writeFileSync(outputPath, JSON.stringify(transcript, null, 2));
console.log(`✅ Transcript saved: ${outputPath}`);
}
module.exports = {
name: "assembly",
type: "transcription",
displayname: "AssemblyAI",
run: async function(audioPath) {
try {
// Determine if audioPath is an external URL or a local file
let audioUrl;
if (/^https?:\/\//i.test(audioPath)) {
console.log('🔗 Using external audio URL...');
audioUrl = audioPath;
} else {
if (!fs.existsSync(audioPath)) {
throw new Error(`Audio file not found: ${audioPath}`);
}
console.log('📤 Uploading audio file...');
audioUrl = await uploadAudio(audioPath);
}
console.log('🔄 Creating transcript job...');
const transcriptId = await createTranscript(audioUrl);
console.log('⏳ Waiting for transcription...');
const transcript = await pollTranscript(transcriptId);
const sessionId = getSessionId(audioPath);
saveTranscript(transcript, sessionId);
return transcript;
} catch (error) {
console.error('❌ Transcription error:', error.message);
throw error;
}
}
};
@@ -0,0 +1,133 @@
import 'dotenv/config';
import axios from 'axios';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const API_KEY = process.env.ASSEMBLYAI_API_KEY;
const BASE_URL = 'https://api.assemblyai.com/v2';
/**
* Uploads audio file to AssemblyAI
*/
async function uploadAudio(audioPath: string): Promise<string> {
const audioData = fs.readFileSync(audioPath);
const response = await axios.post<{ upload_url: string }>(`${BASE_URL}/upload`, audioData, {
headers: {
'authorization': API_KEY,
'content-type': 'application/octet-stream'
}
});
return response.data.upload_url;
}
/**
* Extract a session id (basename without extension) from a local path or a URL
*/
function getSessionId(inputPath: string): string {
try {
const parsed = new URL(inputPath);
const base = path.basename(parsed.pathname);
return base.replace(/\.[^.]+$/, '');
} catch (err) {
// not a URL, treat as local path
return path.basename(inputPath, path.extname(inputPath));
}
}
/**
* Creates transcription job with speaker diarization
*/
async function createTranscript(audioUrl: string): Promise<string> {
const response = await axios.post<{ id: string }>(`${BASE_URL}/transcript`, {
audio_url: audioUrl,
speaker_labels: true,
language_detection: true
}, {
headers: {
'authorization': API_KEY,
'content-type': 'application/json'
}
});
return response.data.id;
}
/**
* Polls transcript status until completed
*/
async function pollTranscript(transcriptId: string): Promise<any> {
while (true) {
const response = await axios.get<any>(`${BASE_URL}/transcript/${transcriptId}`, {
headers: { 'authorization': API_KEY }
});
const status = response.data.status;
if (status === 'completed') {
return response.data;
} else if (status === 'error') {
throw new Error(`Transcription failed: ${response.data.error}`);
}
// Wait 3 seconds before next poll
await new Promise(resolve => setTimeout(resolve, 3000));
}
}
/**
* Saves transcript to storage
*/
function saveTranscript(transcript: any, sessionId: string): void {
const outputDir = path.join(__dirname, '..', '..', '..', 'storage', 'transcripts');
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
const outputPath = path.join(outputDir, `${sessionId}.json`);
fs.writeFileSync(outputPath, JSON.stringify(transcript, null, 2));
console.log(`✅ Transcript saved: ${outputPath}`);
}
export default {
name: "assembly",
type: "transcription",
displayname: "AssemblyAI",
run: async (audioPath: string) => {
try {
// Determine if audioPath is an external URL or a local file
let audioUrl: string;
if (/^https?:\/\//i.test(audioPath)) {
console.log('🔗 Using external audio URL...');
audioUrl = audioPath;
} else {
console.log('🔄 Uploading local audio...');
if (!fs.existsSync(audioPath)) {
throw new Error(`Audio file not found: ${audioPath}`);
}
audioUrl = await uploadAudio(audioPath);
}
console.log('🔄 Creating transcript job...');
const transcriptId = await createTranscript(audioUrl);
console.log('⏳ Waiting for transcription...');
const transcript = await pollTranscript(transcriptId);
const sessionId = getSessionId(audioPath);
saveTranscript(transcript, sessionId);
return transcript;
} catch (error: any) {
console.error('❌ Transcription error:', error.message);
throw error;
}
}
};
@@ -1,8 +0,0 @@
module.exports = {
name:"assembly", // Unique name for our function that will later be used to get the function from the map via "mapFunctions.get("example").function()"
type:"transcription", // value used to differentiate each module to order them in the UI
displayname:"Assembly", // The displayname used within the UI
async function(parameter){
// TODO add code to actually process the audio file
}
}
Submodule services/modules/transcription/local/whisper.cpp deleted from 999a7e0cbf
@@ -0,0 +1,53 @@
// services/pipeline/jobs/transcribeLatest.ts
import path from 'path';
import fs from 'fs';
// @ts-ignore: module has no type declarations or cannot be resolved in current TS config
import assembly from '../../modules/transcription/assembly';
/**
* Finds the most recently modified .wav file in storage/audio/
*/
function getLatestWav(): string {
const audioDir = path.join(process.cwd(), 'storage', 'audio');
const files = fs.readdirSync(audioDir).filter(f => f.toLowerCase().endsWith('.wav'));
if (files.length === 0) throw new Error('⚠️ No .wav file found in storage/audio');
const newest = files
.map(f => ({ f, t: fs.statSync(path.join(audioDir, f)).mtimeMs }))
.sort((a, b) => b.t - a.t)[0].f;
return path.join(audioDir, newest);
}
/**
* Full transcription pipeline according to the defined workflow:
* 1. Audio Upload → AssemblyAI
* 2. Job Creation (transcript_id)
* 3. Polling Status (queued → processing → completed)
* 4. Download Transcript JSON
* 5. Storage: /transcripts/{session_id}.json
*/
async function main() {
const audioPath = getLatestWav();
console.log('1️⃣ Audio Upload → AssemblyAI');
console.log(' Source:', audioPath);
console.log('2️⃣ Job Creation (transcript_id)');
console.log('3️⃣ Polling Status (queued → processing → completed)');
console.log('4️⃣ Download Transcript JSON');
console.log('5️⃣ Storage: /transcripts/{session_id}.json');
// Execute the transcription process via the AssemblyAI module
const result = await assembly.run(audioPath);
console.log('✅ Transcription completed successfully');
console.log('🆔 Transcript ID:', result.id);
console.log('📁 Transcript file saved under: storage/transcripts/');
}
// Entry point
main().catch((err) => {
console.error('❌ Transcription pipeline failed:', err.message || err);
process.exit(1);
});
View File
View File
+14
View File
@@ -0,0 +1,14 @@
import 'dotenv/config';
import assemblyModule from '../../services/modules/transcription-remote/assembly.ts';
// Test: URL passed as argument OR local file ./storage/audio/test.wav
const audioPath = process.argv[2] || './storage/audio/test.wav';
assemblyModule.run(audioPath)
.then(result => {
console.log('✅ Success!');
console.log('Transcript ID:', result.id);
})
.catch(error => {
console.error('❌ Error:', error?.message || error);
});
+8
View File
@@ -0,0 +1,8 @@
from fastapi.testclient import TestClient
from app.main import app
client = TestClient(app)
def test_health():
response = client.get("/health")
assert response.status_code == 200
View File