Compare commits

..

10 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
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
eric.minning 1a681eb2b8 Improved error log and implemented a variable and function for file path storage 2025-11-12 20:14:56 +01:00
14 changed files with 506 additions and 169 deletions
BIN
View File
Binary file not shown.
+7 -5
View File
@@ -10,9 +10,10 @@
<div class="mitte" id="mitte">
<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>
<button class="eng_Btn" id="eng_Btn" onclick="changeLanguage('en')"><img src="flags/united-kingdom-flag-png-large.jpg" width="25px" height="20px"/></button>
<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="ai_type" id="ai_type">
</select>
<select name="language_option" id="language_option">
</select>
</div>
<h1 id="h1">Video to document</h1>
@@ -46,7 +47,8 @@
</div>
</div>
<script src="script.js"></script>
<script src="./renderer.js"></script>
<script src="languages.js"></script>
<script src="script.js"></script>
<script src="./renderer.js"></script>
</body>
</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 path from 'path';
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 __dirname = path.dirname(__filename);
@@ -22,7 +25,61 @@ function createWindow() {
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)
ipcMain.handle('convert-video', async (event, filePath) => {
+1 -4
View File
@@ -1,5 +1,3 @@
const { contextBridge, ipcRenderer, webUtils } = require('electron')
try {
@@ -14,5 +12,4 @@ try {
})
} catch (error) {
console.log("Error in preload.js");
}
}
+14 -4
View File
@@ -25,7 +25,17 @@ uploadContainer.addEventListener("drop", (e) => {
}
} catch (error) {
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
manualUploadBtn.addEventListener('click', () => {
try {
@@ -40,40 +39,22 @@ function checkBoxes() {
//language changing feature
function changeLanguage(language) {
if (language === 'en') {
document.getElementById('title').textContent = 'Video to document';
document.getElementById('h1').textContent = 'Video to document';
document.getElementById('p1').textContent = 'Drag and drop video file';
document.getElementById('fileName').textContent = 'No video chosen';
document.getElementById('manualUploadBtn').textContent = 'Search video';
document.getElementById('checkbox_group').textContent = 'Choose prefered document style:';
document.getElementById('label_format').textContent = 'Meeting report';
document.getElementById('label_summary').textContent = 'Summary with timestamps';
document.getElementById('submitButton').textContent = 'Submit';
} else if (language === 'de') {
document.getElementById('title').textContent = 'Video zu Dokument';
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 = 'जमा करना';
}
try {
document.getElementById('title').textContent = languageOptions[language].title;
document.getElementById('h1').textContent = languageOptions[language].h1;
document.getElementById('p1').textContent = languageOptions[language].p1;
document.getElementById('fileName').textContent = languageOptions[language].fileName;
document.getElementById('manualUploadBtn').textContent = languageOptions[language].manualUploadBtn;
document.getElementById('checkbox_group').textContent = languageOptions[language].checkbox_group;
document.getElementById('label_format').textContent = languageOptions[language].label_format;
document.getElementById('label_summary').textContent = languageOptions[language].label_summary;
document.getElementById('submitButton').textContent = languageOptions[language].submitButton;
} catch (error) {
console.log(error);
}
}
//listener for the file explorer search when something got selected
videoUpload.addEventListener('change', () => {
try {
@@ -84,8 +65,6 @@ videoUpload.addEventListener('change', () => {
});
//function to display the file path in the drop down box
function handleFiles(files) {
try {
@@ -112,4 +91,42 @@ function updateProgressBar(bar, value){
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("--------------------------------------------------------------------------------");
// ======================== 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 --------------------------------------------------------- //
+132 -2
View File
@@ -10,6 +10,7 @@
"license": "ISC",
"dependencies": {
"@types/axios": "^0.9.36",
"axios": "^1.13.2",
"cli-progress": "^3.12.0",
"dotenv": "^17.2.3",
"electron": "^39.1.1",
@@ -206,7 +207,6 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.2.tgz",
"integrity": "sha512-uWN8YqxXxqFMX2RqGOrumsKeti4LlmIMIyV0lgut4jx7KQBcBiW6vkDtIBvHnHIquwNfJhk8v2OtmO8zXWHfPA==",
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
@@ -299,6 +299,23 @@
"resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
"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": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
@@ -428,6 +445,18 @@
"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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
@@ -569,6 +598,15 @@
"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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -711,6 +749,21 @@
"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": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz",
@@ -855,6 +908,63 @@
"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": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
@@ -1049,6 +1159,21 @@
"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": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
@@ -1401,6 +1526,12 @@
"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": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz",
@@ -1845,7 +1976,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
+1
View File
@@ -1,6 +1,7 @@
{
"dependencies": {
"@types/axios": "^0.9.36",
"axios": "^1.13.2",
"cli-progress": "^3.12.0",
"dotenv": "^17.2.3",
"electron": "^39.1.1",
-109
View File
@@ -1,109 +0,0 @@
Generate a structured meeting report in MARKDOWN using STRUCTURE and STYLE.
Output ONLY the final .md document — no meta comments, no explanations.
Follow exactly the STRUCTURE defined below.
Follow exactly the STYLE rules.
Use timestamps in HH:MM:SS format.
If information is missing, use: UNKLAR:<reason>.
==================== STRUCTURE & RULES ====================
{
"FORMAT": "markdown",
"STRUCTURE": {
"titlepage": [
"title",
"date",
"start",
"end",
"duration",
"location",
"host",
"participants"
],
"toc": "[section](#anchor) — HH:MM:SS",
"section": {
"h2": "<topic> — HH:MM:SS",
"summary": "exactly 1 concise sentence",
"key_points": "maximum 5 bullet points; quotes optional",
"decisions": "list items formatted as: decision text | owner | due date",
"actions": "markdown table: id | task | owner | due | status"
},
"exec_summary": "exactly 3 short sentences",
"consolidated": [
"decisions",
"actions"
],
"appendix": "optional"
},
"STYLE": {
"tone": "neutral, concise, professional",
"ts_format": "HH:MM:SS",
"no_meta": true
},
"PROCESS": {
"timestamps": "use transcript timestamps if present; otherwise estimate minimal",
"speakers": "use names if available; else Speaker X",
"long_transcripts": "split → summarize → merge",
"unclear": "UNKLAR:<reason>"
},
"PROMPT_SNIPPET": "Generate meeting report in markdown using STRUCTURE and STYLE. Output only the report."
}
============================================================
Insert all generated content into the following MARKDOWN TEMPLATE:
# {{title}}
**Date:** {{date}}
**Start:** {{start}}
**End:** {{end}}
**Duration:** {{duration}}
**Location:** {{location}}
**Host:** {{host}}
**Participants:** {{participants}}
---
## Table of Contents
{{toc}}
---
## Executive Summary
{{exec_summary}}
---
## Sections
{{sections}}
---
## Consolidated Decisions
{{consolidated_decisions}}
---
## Consolidated Actions
{{consolidated_actions}}
---
## Appendix
{{appendix}}
============================================================
Final Requirement:
Output ONLY the completed Markdown meeting report.
+10 -2
View File
@@ -1,3 +1,5 @@
const EventEmitter = require('events');
const audioEvents = new EventEmitter();
// Ensure ffmpeg binary is available
if (!ffmpegPath) {
@@ -75,6 +77,12 @@ module.exports = {
progressBar.update(100, { timemark: 'done' });
progressBar.stop();
console.log(`Extraction completed: ${outputAudioPath}`);
audioEvents.emit('audio_ready', {
sessionId: inputVideoName,
audioPath: outputAudioPath
});
resolve();
})
.on('error', (err) => {
@@ -88,6 +96,6 @@ module.exports = {
console.log();
}
});
}
},
audioEvents: audioEvents
}
@@ -1,8 +1,128 @@
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
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;
}
}
};
@@ -1,6 +1,7 @@
// 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';
/**