Merge branch 'feature/speaker_renaming' into 'develop'

Remade replace Speaker module which now takes the premade speaker mapping json...

See merge request proj-wise2526-video2document/video2document!89
This commit is contained in:
Hughes, Mike
2026-01-15 14:36:24 +01:00
8 changed files with 459 additions and 224 deletions
Vendored
BIN
View File
Binary file not shown.
+138 -92
View File
@@ -1,5 +1,5 @@
// Loading required packages
require("./requires.js")
require("./requires.js");
console.log(start);
const https = require("https");
@@ -55,16 +55,17 @@ req.end();
// Initialising map to be used to store the functionality later on for reloadability
mapFunctions = new Map()
mapFunctions = new Map();
// Loading the Function Map
var path = `${mainDir}/services/modules`
var path = `${mainDir}/services/modules`;
var folders = fs.readdirSync(path).filter(function (file) {
return fs.statSync(path+'/'+file).isDirectory();
return fs.statSync(path + "/" + file).isDirectory();
});
folders.forEach(element => {
var commandFiles = fs.readdirSync(`${path}/${element}`).filter(file => file.endsWith('.js') && !file.startsWith("index"));
folders.forEach((element) => {
var commandFiles = fs
.readdirSync(`${path}/${element}`)
.filter((file) => file.endsWith(".js") && !file.startsWith("index"));
for (const file of commandFiles) {
delete require.cache[require.resolve(`${path}/${element}/${file}`)];
const command = require(`${path}/${element}/${file}`);
@@ -74,11 +75,13 @@ folders.forEach(element => {
// The startup information for the project, here you can add stuff that might be nice to see when the app starts
mapFunctions.get("Startup_function").function()
console.log("------------------------------------ Status ------------------------------------");
mapFunctions.get("Startup_function").function();
console.log(
"------------------------------------ Status ------------------------------------"
);
console.log(__dirname);
console.log(platform);
console.log(`The Startup took ${new Date() - start}ms`)
console.log(`The Startup took ${new Date() - start}ms`);
console.log(`${mapFunctions.size} Function modules loaded`);
console.log("--------------------------------------------------------------------------------");
@@ -93,11 +96,11 @@ function createWindow() {
webPreferences: {
nodeIntegration: false,
contextIsolation: true,
preload: `${mainDir}/electron/main/preload.js`
}
preload: `${mainDir}/electron/main/preload.js`,
},
});
mainWindow.loadFile('./electron/main/index.html');
mainWindow.loadFile("./electron/main/index.html");
}
electron.app.whenReady().then(createWindow);
@@ -107,21 +110,11 @@ electron.ipcMain.handle('get-module-names', async () => {
"ai_modules":[],
"transcription_modules":[]
}
mapFunctions.forEach(e => {
switch(e.type){
case "llm":
module_array.ai_modules.push({"name": e.name, "displayname": e.displayname})
break;
case "transcription":
module_array.transcription_modules.push({"name": e.name, "displayname": e.displayname})
break;
}
})
});
// console.log(module_array);
return module_array
return module_array;
});
// electron.ipcMain.on("get_modules", async (event, args) => {
// let module_array = {
// "ai_modules":[],
@@ -142,21 +135,21 @@ electron.ipcMain.handle('get-module-names', async () => {
// mainWindow.webContents.send("modules", module_array)
// })
var globalArgs = {}
var globalFinalHtmlPath = ""
var globalArgs = {};
var globalFinalHtmlPath = "";
electron.ipcMain.on("file_submit", async (event, args) => {
try {
globalArgs = args
let curstep = 0
let totalsteps = 4
globalArgs = args;
let curstep = 0;
let totalsteps = 4;
const TEMPLATE_MAP = {
"followup-report": "followup_report.txt",
"agenda": "agenda.txt",
agenda: "agenda.txt",
"result-protocol": "result_protocol.txt",
"sprint-planning": "sprint_planning_note.txt",
"custom": "custom_document.txt"
custom: "custom_document.txt",
};
const templateFile = TEMPLATE_MAP[args.document.type];
@@ -166,106 +159,159 @@ electron.ipcMain.on("file_submit", async (event, args) => {
}
console.log(args);
let audiopath = ""
let transcriptpath = ""
let audiopath = "";
let transcriptpath = "";
console.log("\n\n Running the Video to Audio Extractor");
// This code handles the Video to Audio extraction module call
await mapFunctions.get("module-handler").function(args.video.module, {inputVideoPath: args.video.inputVideoPath, outputType: mapFunctions.get(args.transcription.module).audioformat}).then(resp => {
console.log(resp);
audiopath = resp
curstep++
mainWindow.webContents.send("progress", {curstep:curstep, totalsteps:totalsteps})
}).catch(err => {
mainWindow.webContents.send("error", err)
console.log(err);
return
await mapFunctions
.get("module-handler")
.function(args.video.module, {
inputVideoPath: args.video.inputVideoPath,
outputType: mapFunctions.get(args.transcription.module).audioformat,
})
console.log("\n\n Running the Audio to Transcription module");
// TODO implement transcription module
// This code handles the Audio to Text transcription module call
await mapFunctions.get("module-handler").function(args.transcription.module, audiopath).then(resp => {
.then((resp) => {
console.log(resp);
transcriptpath = resp
curstep++
mainWindow.webContents.send("progress", {curstep:curstep, totalsteps:totalsteps})
}).catch(err => {
mainWindow.webContents.send("error", err)
console.log(err);
return
audiopath = resp;
curstep++;
mainWindow.webContents.send("progress", {
curstep: curstep,
totalsteps: totalsteps,
});
})
.catch((err) => {
mainWindow.webContents.send("error", err);
console.log(err);
return;
});
console.log("\n\n Running the Transcription Summarizer module");
// This code summarises the transcript, so that it can be used by an llm
// await mapFunctions.get("summarize-transcription").function('A:\\programing\\@projects\\video2document\\storage\\transcripts\\IMG_2978.json').then(resp => {
await mapFunctions.get("summarize-transcription2").function(transcriptpath).then(resp => {
await mapFunctions
.get("summarize-transcription2")
.function(transcriptpath)
.then((resp) => {
console.log(resp);
transcriptpath = resp
curstep++
mainWindow.webContents.send("progress", {curstep:curstep, totalsteps:totalsteps})
}).catch(err => {
mainWindow.webContents.send("error", err)
console.log(err);
return
transcriptpath = resp;
curstep++;
mainWindow.webContents.send("progress", {
curstep: curstep,
totalsteps: totalsteps,
});
})
.catch((err) => {
mainWindow.webContents.send("error", err);
console.log(err);
return;
});
console.log("\n\n Running the LLM module");
// TODO implement documentation module
// This code handles the Text to Document processing module call
console.log(`\n\n Running the LLM for Document Style ${args.document.type}`);
console.log(
`\n\n Running the LLM for Document Style ${args.document.type}`
);
await mapFunctions.get("module-handler").function(args.document.module, { inputTranscriptPath: transcriptpath, documentTypePath: "./storage/documentType/" + templateFile, language: "en" }).then(resp => {
await mapFunctions
.get("module-handler")
.function(args.document.module, {
inputTranscriptPath: transcriptpath,
documentTypePath: "./storage/documentType/" + templateFile,
language: "en",
})
.then((resp) => {
console.log(resp);
globalFinalHtmlPath = resp
curstep++
mainWindow.webContents.send("progress", {curstep:curstep, totalsteps:totalsteps})
}).catch(err => {
mainWindow.webContents.send("error", err)
console.log(err);
return
globalFinalHtmlPath = resp;
curstep++;
mainWindow.webContents.send("progress", {
curstep: curstep,
totalsteps: totalsteps,
});
})
await mapFunctions.get("extract-speaker-snippets").function({audioPath: audiopath, jsonPath: transcriptpath }).then(resp => {
mainWindow.webContents.send("speakerAudios", resp)
}).catch(err => {
mainWindow.webContents.send("error", err)
.catch((err) => {
mainWindow.webContents.send("error", err);
console.log(err);
return
return;
});
await mapFunctions
.get("extract-speaker-snippets")
.function({ audioPath: audiopath, jsonPath: transcriptpath })
.then((resp) => {
mainWindow.webContents.send("speakerAudios", resp);
})
.catch((err) => {
mainWindow.webContents.send("error", err);
console.log(err);
return;
});
} catch (error) {
console.log(error);
}
})
});
electron.ipcMain.on("file_download", async() => {
await mapFunctions.get("htmlDocumentConverter").convert({inputPath:globalFinalHtmlPath, format: globalArgs.document.outputType, showDialog: true});
})
electron.ipcMain.on("file_download", async () => {
await mapFunctions
.get("htmlDocumentConverter")
.convert({
inputPath: globalFinalHtmlPath,
format: globalArgs.document.outputType,
showDialog: true,
});
});
electron.ipcMain.on("speaker_submit", async() => {
electron.ipcMain.on("speaker_submit", async (event, args) => {
console.log("\n\n\nJa also hier kam was an \n\n\n");
})
console.log(args);
try {
await mapFunctions.get("replace_speaker").function(args);
event.reply("speaker_submit_response", { success: true });
} catch (error) {
console.error("Error:", error);
event.reply("speaker_submit_response", {
success: false,
error: error.message,
});
}
});
let q = {
video: {
module: "String", // The name of the module, idk if we ever implement other extraction modules, the default one is extraction-video-to-audio
inputVideoPath: "String", // See script.js on line 27 for an example of what this should look like
outputType: "String", // The file format to be used for the audio output file, such as wav, mp3, flac and so on
},
transcription: {
module: "String", // The module name of the transcription model you want to use
},
document: {
module: "String", // The module name of the AI model you want to use to create the document
styles: [
// An array of all the document styles/prompts you want to have the document be processed with
{
prompt: "String",
},
],
},
};
//gibt Documentfiles an preload zurück
electron.ipcMain.handle('get-txt-files', () => {
const storagePath = `${mainDir}/storage/documentType`
electron.ipcMain.handle("get-txt-files", () => {
const storagePath = `${mainDir}/storage/documentType`;
return fs.readdirSync(storagePath)
.filter(f => f.endsWith('.txt'))
return fs.readdirSync(storagePath).filter((f) => f.endsWith(".txt"));
});
//speichern neuer document types
electron.ipcMain.handle('save-txt-file', (event, fileName, content) => {
electron.ipcMain.handle("save-txt-file", (event, fileName, content) => {
const filePath = `${mainDir}/storage/documentType/${fileName}.txt`;
fs.writeFileSync(filePath, content, 'utf8');
fs.writeFileSync(filePath, content, "utf8");
return true;
});
@@ -0,0 +1,79 @@
const fs = require('fs');
const path = require('path');
const module_exports = {
name: "replace_speaker",
type: "processor",
displayname: "Speaker Name Replacer",
description: "Replaces speaker placeholder names with actual names based on a mapping in HTML files",
async function(speakerMapping) {
// Relativ von dieser Datei aus
const documentsDir = path.resolve(__dirname, '../../../storage/documents');
const inputHtmlPath = await this.getNewestFile(documentsDir, '.html');
if (!inputHtmlPath) {
throw new Error(`No HTML files found in ${documentsDir}`);
}
return await this.replaceNames(inputHtmlPath, speakerMapping);
},
getNewestFile: async function(dirPath, extension) {
try {
const files = await fs.promises.readdir(dirPath);
const filtered = files.filter(f => f.endsWith(extension));
if (filtered.length === 0) return null;
const filesWithStats = await Promise.all(
filtered.map(async (f) => {
const fullPath = path.join(dirPath, f);
const stats = await fs.promises.stat(fullPath);
return { path: fullPath, time: stats.mtimeMs };
})
);
return filesWithStats.reduce((newest, curr) =>
curr.time > newest.time ? curr : newest
).path;
} catch (error) {
console.error("Error reading directory:", error);
throw error;
}
},
replaceNames: async function(inputHtmlPath, speakerMapping) {
try {
const htmlContent = await fs.promises.readFile(inputHtmlPath, "utf-8");
let outputContent = htmlContent;
Object.entries(speakerMapping).forEach(([placeholder, value]) => {
// Extract name if value is an object
const displayName = typeof value === 'string' ? value : value.name;
// Normalize placeholder for matching (remove case sensitivity)
const normalizedPlaceholder = placeholder.toLowerCase();
// Replace all variations: speakerA, SpeakerA, SPEAKERA, speaker_a, Speaker A, etc.
// Matches with optional spaces, underscores, and parentheses
const regex = new RegExp(
`\\b[Ss]peaker\\s*[_-]?\\s*${placeholder.charAt(placeholder.length - 1)}\\b|\\b${placeholder}\\b`,
'gi'
);
outputContent = outputContent.replace(regex, displayName);
});
await fs.promises.writeFile(inputHtmlPath, outputContent, "utf-8");
return inputHtmlPath;
} catch (error) {
console.error("Error replacing speaker names:", error);
throw error;
}
}
};
module.exports = module_exports;
+32 -13
View File
@@ -1,26 +1,45 @@
# Meeting Agenda Generator
Du bist ein erfahrener Moderator und Projektmanager.
AUFGABE:
Erstelle eine sinnvolle Meeting-Agenda basierend auf dem folgenden Transkript.
## KRITISCH - SPRECHER-PLATZHALTER BEWAHREN:
**speakerA, speakerB, speakerC, etc. sind technische Platzhalter und DÜRFEN NICHT verändert werden.**
- RICHTIG: "speakerA hat entschieden..."
- FALSCH: "Speaker A", "speakerA (Name)", Klammern, Leerzeichen, Ergänzungen
ANFORDERUNGEN:
Diese Token werden später durch echte Namen ersetzt. Jede Änderung bricht diesen Prozess.
---
## AUFGABE:
Erstelle eine sinnvolle Meeting-Agenda basierend auf dem Meeting-Transkript.
## ANFORDERUNGEN:
- Rekonstruiere die tatsächlichen Themenblöcke
- Ordne sie logisch und chronologisch
- Fasse ähnliche Diskussionen zusammen
- Keine irrelevanten Details aufnehmen
- Speicher-Platzhalter (speakerA, speakerB, etc.) exakt beibehalten
STRUKTUR:
- Titel der Agenda
- Ziel des Meetings (12 Sätze)
- Agenda-Punkte (nummeriert)
- Thema
- Kurzbeschreibung
- Ziel des Punktes (Information, Entscheidung, Diskussion)
## STRUKTUR:
STIL:
### 1. Titel der Agenda
### 2. Ziel des Meetings
- 12 präzise Sätze
### 3. Agenda-Punkte (nummeriert)
Für jeden Punkt:
- **Thema**
- **Kurzbeschreibung**
- **Ziel des Punktes:** (Information / Entscheidung / Diskussion)
## STIL:
- Klar, kompakt
- Business-orientiert
- Keine Sprecher- oder Zeitangaben
- Namen aus dem Transkript speakerA, speakerB etc. sollen weiterhin bestehen bleiben wie sie sind und nicht im Dokument ersetzt werden
- Logische Reihenfolge
TRANSKRIPT:
---
**TRANSKRIPT:**
+34 -12
View File
@@ -1,22 +1,44 @@
# Custom Document Generator
Du bist ein intelligenter Dokumenten-Generator.
AUFGABE:
## KRITISCH - SPRECHER-PLATZHALTER BEWAHREN:
**speakerA, speakerB, speakerC, etc. sind technische Platzhalter und DÜRFEN NICHT verändert werden.**
- RICHTIG: "speakerA hat entschieden..."
- FALSCH: "Speaker A", "speakerA (Name)", Klammern, Leerzeichen, Ergänzungen
Diese Token werden später durch echte Namen ersetzt. Jede Änderung bricht diesen Prozess.
---
## AUFGABE:
Erstelle ein individuelles Dokument basierend auf:
1) dem Meeting-Transkript
2) der zusätzlichen Nutzeranweisung
1. dem Meeting-Transkript
2. der zusätzlichen Nutzeranweisung
WICHTIG:
- Priorisiere die Nutzeranweisung
## ANFORDERUNGEN:
- **Priorisiere die Nutzeranweisung** über standardisierte Strukturen
- Nutze das Transkript als Wissensquelle
- Struktur, Tonalität und Detailgrad anpassen
- Struktur, Tonalität und Detailgrad nach Nutzervorgabe anpassen
- Inhalte logisch zusammenführen
- Namen aus dem Transkript speakerA, speakerB etc. sollen weiterhin bestehen bleiben wie sie sind und nicht im Dokument ersetzt werden
- Speicher-Platzhalter (speakerA, speakerB, etc.) exakt beibehalten
FORMAT:
- Passe Struktur und Stil an den Nutzerwunsch an
- Klare Überschriften
- Keine Sprecher- oder Zeitangaben
## VORGEHEN:
1. Lese die Nutzeranweisung sorgfältig
2. Extrahiere aus dem Transkript die relevanten Informationen
3. Erstelle das Dokument nach der gewünschten Struktur
4. Behalte Sprecher-Platzhalter unverändert
TRANSKRIPT & NUTZERANWEISUNG:
## ALLGEMEINE RICHTLINIEN:
- Entferne Redundanzen und Smalltalk
- Keine Zeitstempel oder Sprecherangaben (wenn nicht gefordert)
- Sachlich, präzise, professionell
- Keine Spekulationen oder Meinungen (wenn nicht gefordert)
---
**NUTZERANWEISUNG:**
[Hier kommt die Anforderung des Nutzers hin]
**TRANSKRIPT:**
[Hier kommt das Meeting-Transkript hin]
+41 -25
View File
@@ -1,45 +1,61 @@
# Follow-up Report Generator
Du bist ein professioneller Meeting-Analyst und Business Writer.
AUFGABE:
Erstelle einen strukturierten Follow-up Report basierend auf dem folgenden Meeting-Transkript.
## KRITISCH - SPRECHER-PLATZHALTER BEWAHREN:
**speakerA, speakerB, speakerC, etc. sind technische Platzhalter und DÜRFEN NICHT verändert werden.**
- RICHTIG: "speakerA hat entschieden..."
- FALSCH: "Speaker A", "speakerA (Name)", Klammern, Leerzeichen, Ergänzungen
ANFORDERUNGEN:
Diese Token werden später durch echte Namen ersetzt. Jede Änderung bricht diesen Prozess.
---
## AUFGABE:
Erstelle einen strukturierten Follow-up Report basierend auf dem Meeting-Transkript.
## ANFORDERUNGEN:
- Fasse Inhalte sinngemäß zusammen
- Entferne Redundanzen und Smalltalk
- Formuliere klar, präzise und professionell
- Verwende neutrale Business-Sprache
- Keine Zeitstempel oder Sprecher-Namen zitieren
- Leite Entscheidungen und Aufgaben logisch ab, wenn sie implizit sind
- Markiere offene Punkte klar
- Namen aus dem Transkript speakerA, speakerB etc. sollen weiterhin bestehen bleiben wie sie sind und nicht im Dokument ersetzt werden
- Speicher-Platzhalter (speakerA, speakerB, etc.) exakt beibehalten
STRUKTUR DES DOKUMENTS:
1. Titel & Metadaten
- Meetingtitel (ableiten)
- Datum (falls im Transkript erwähnt, sonst „nicht angegeben“)
- Teilnehmer (zusammengefasst)
## STRUKTUR DES DOKUMENTS:
2. Executive Summary (max. 5 Bullet Points)
### 1. Titel & Metadaten
- Meetingtitel (ableiten)
- Datum (falls im Transkript erwähnt, sonst „nicht angegeben")
- Teilnehmer (zusammengefasst)
3. Besprochene Themen
- Thema
- Kernaussagen
- Relevante Erkenntnisse
### 2. Executive Summary
- Max. 5 Bullet Points
- Kernpunkte zusammenfassen
4. Entscheidungen
- Entscheidung
- Kontext / Begründung
### 3. Besprochene Themen
- Thema
- Kernaussagen
- Relevante Erkenntnisse
5. Action Items
- Aufgabe
- Verantwortlich (falls ableitbar)
- Ziel / Zweck
### 4. Entscheidungen
- Entscheidung
- Kontext / Begründung
6. Offene Fragen & Risiken
### 5. Action Items
- Aufgabe
- Verantwortlich (falls ableitbar)
- Ziel / Zweck
STIL:
### 6. Offene Fragen & Risiken
## STIL:
- Überschriften klar strukturiert
- Bullet Points bevorzugen
- Präzise, keine Umgangssprache
- Keine Zeitstempel oder Sprecherangaben
TRANSKRIPT:
---
**TRANSKRIPT:**
+40 -13
View File
@@ -1,27 +1,54 @@
# Result Protocol Generator
Du bist ein professioneller Protokollführer.
AUFGABE:
## KRITISCH - SPRECHER-PLATZHALTER BEWAHREN:
**speakerA, speakerB, speakerC, etc. sind technische Platzhalter und DÜRFEN NICHT verändert werden.**
- RICHTIG: "speakerA hat entschieden..."
- FALSCH: "Speaker A", "speakerA (Name)", Klammern, Leerzeichen, Ergänzungen
Diese Token werden später durch echte Namen ersetzt. Jede Änderung bricht diesen Prozess.
---
## AUFGABE:
Erstelle ein Ergebnisprotokoll basierend auf dem Meeting-Transkript.
FOKUS:
## FOKUS:
- Ergebnisse statt Diskussionen
- Entscheidungen, Beschlüsse, Vereinbarungen
- Klare, überprüfbare Aussagen
- Speicher-Platzhalter (speakerA, speakerB, etc.) exakt beibehalten
STRUKTUR:
1. Meeting-Informationen
2. Ergebnisse je Thema
- Thema
- Ergebnis / Beschluss
3. Entscheidungen
4. Aufgaben & Verantwortlichkeiten
5. Offene Punkte
## STRUKTUR:
REGELN:
### 1. Meeting-Informationen
- Titel
- Datum
- Teilnehmer
### 2. Ergebnisse je Thema
- **Thema**
- **Ergebnis / Beschluss**
### 3. Entscheidungen
- Klare Entscheidungen mit Kontext
### 4. Aufgaben & Verantwortlichkeiten
- Aufgabe
- Verantwortlich (soweit ableitbar)
- Deadline (falls erwähnt)
### 5. Offene Punkte
- Ungelöste Fragen
- Ausstehende Klärungen
## REGELN:
- Keine Meinungen oder Spekulationen
- Keine Zeit- oder Sprecherangaben
- Sachlich, formal
- Namen aus dem Transkript speakerA, speakerB etc. sollen weiterhin bestehen bleiben wie sie sind und nicht im Dokument ersetzt werden
- Fokus auf Fakten und Ergebnisse
---
TRANSKRIPT:
**TRANSKRIPT:**
+43 -17
View File
@@ -1,35 +1,61 @@
# Sprint Planning Note Generator
Du bist ein erfahrener Scrum Master.
AUFGABE:
Erstelle Sprint Planning Notes aus dem folgenden Meeting-Transkript.
## KRITISCH - SPRECHER-PLATZHALTER BEWAHREN:
**speakerA, speakerB, speakerC, etc. sind technische Platzhalter und DÜRFEN NICHT verändert werden.**
- RICHTIG: "speakerA hat entschieden..."
- FALSCH: "Speaker A", "speakerA (Name)", Klammern, Leerzeichen, Ergänzungen
FOKUS:
Diese Token werden später durch echte Namen ersetzt. Jede Änderung bricht diesen Prozess.
---
## AUFGABE:
Erstelle Sprint Planning Notes aus dem Meeting-Transkript.
## FOKUS:
- Sprint-Ziele
- User Stories / Tasks
- Abhängigkeiten
- Risiken
- Commitments
- Speicher-Platzhalter (speakerA, speakerB, etc.) exakt beibehalten
STRUKTUR:
1. Sprint Overview
- Sprint-Ziel
- Zeitraum (falls erwähnt)
## STRUKTUR:
2. Geplante Arbeit
- User Story / Task
- Beschreibung
- Akzeptanzkriterien (falls ableitbar)
### 1. Sprint Overview
- **Sprint-Ziel:** (Kurz und prägnant)
- **Zeitraum:** (Falls im Transkript erwähnt)
3. Abhängigkeiten & Blocker
### 2. Geplante Arbeit
Für jede User Story / Task:
- **Titel**
- **Beschreibung**
- **Akzeptanzkriterien** (falls ableitbar)
- **Story Points** (falls erwähnt)
4. Risiken & Annahmen
### 3. Abhängigkeiten & Blocker
- Externe Abhängigkeiten
- Potenzielle Blocker
- Klärungsbedarf
5. Vereinbarungen / Team-Commitments
### 4. Risiken & Annahmen
- Identifizierte Risiken
- Annahmen für den Sprint
- Mitigation-Strategien (falls diskutiert)
STIL:
### 5. Team-Commitments / Vereinbarungen
- Vereinbarte Commitments
- Rollen und Verantwortlichkeiten
- Definition of Done (falls diskutiert)
## STIL:
- Agile-konform
- Klar & umsetzungsorientiert
- Bullet Points bevorzugen
- Namen aus dem Transkript speakerA, speakerB etc. sollen weiterhin bestehen bleiben wie sie sind und nicht im Dokument ersetzt werden
- Keine Zeitstempel oder Sprecherangaben
TRANSKRIPT:
---
**TRANSKRIPT:**