Compare commits

..

7 Commits

13 changed files with 255 additions and 395 deletions
BIN
View File
Binary file not shown.
+1 -58
View File
@@ -2,9 +2,6 @@ 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);
@@ -25,61 +22,7 @@ function createWindow() {
mainWindow.loadFile('main/index.html');
}
// 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();
});
app.whenReady().then(createWindow);
// Kommunikation vom Renderer (Frontend)
ipcMain.handle('convert-video', async (event, filePath) => {
-67
View File
@@ -33,73 +33,6 @@ 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 --------------------------------------------------------- //
+2 -132
View File
@@ -10,7 +10,6 @@
"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",
@@ -207,6 +206,7 @@
"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,23 +299,6 @@
"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",
@@ -445,18 +428,6 @@
"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",
@@ -598,15 +569,6 @@
"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",
@@ -749,21 +711,6 @@
"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",
@@ -908,63 +855,6 @@
"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",
@@ -1159,21 +1049,6 @@
"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",
@@ -1526,12 +1401,6 @@
"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",
@@ -1976,6 +1845,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
-1
View File
@@ -1,7 +1,6 @@
{
"dependencies": {
"@types/axios": "^0.9.36",
"axios": "^1.13.2",
"cli-progress": "^3.12.0",
"dotenv": "^17.2.3",
"electron": "^39.1.1",
+2 -10
View File
@@ -1,5 +1,3 @@
const EventEmitter = require('events');
const audioEvents = new EventEmitter();
// Ensure ffmpeg binary is available
if (!ffmpegPath) {
@@ -77,12 +75,6 @@ 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) => {
@@ -96,6 +88,6 @@ module.exports = {
console.log();
}
});
},
audioEvents: audioEvents
}
}
+76
View File
@@ -0,0 +1,76 @@
const outputDir = path.join(__dirname, "../../../storage/documents"); // path for output directory
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true }); // Create output directory if it doesn't exist
}
// Ensure Google API key is set in environment variables: export GOOGLE_API_KEY="your_api_key_here"
const GEMINI_API_KEY = process.env.GOOGLE_API_KEY; // Ensure Google API key is set in environment variables: export GOOGLE_API_KEY="your_api_key_here"
const GEMINI_URL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent"; //URL for the REST call, used model and action
module.exports = {
name: "llm-gemini",
type: "llm",
displayname: "Gemini LLM",
description: "Generates documents using Google Gemini LLM",
async function(parameter) {
try {
console.log("Gemini LLM module invoked with parameters:", parameter);
await this.createDocumentFromTranscript( //Call the function to create document with transcript, document type and language
parameter.inputTranscriptPath, // Path to input transcript file
parameter.documentTypePath, // Path to document type file which is chosen in the front end by the user
parameter.language // Language for the document which is chosen in the front end by the user
);
} catch (error) {
console.error("Error in Gemini LLM module:", error);
}
},
createDocumentFromTranscript: async function(transcriptPath, documentTypePath, language = "en") { // default language is English
try {
const transcript = await fs.promises.readFile(transcriptPath, "utf-8"); //read transcript file from Path
const documentType = await fs.promises.readFile(documentTypePath, "utf-8"); //read document type from Path
const promptText = `${documentType}, in language ${language}, transcript:\n\n${transcript}`; //combine doc type, language and transcript - Change prompt here if needed
// --- REST CALL ---
const response = await fetch(`${GEMINI_URL}?key=${GEMINI_API_KEY}`, { //safe model response in variable
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({
contents: [
{
parts: [
{ text: promptText } // Input prompt for content generation
]
}
]
})
});
if (!response.ok) { //ok is true when a responce was successfull
const text = await response.text();
throw new Error(`Gemini API error (${response.status}): ${text}`);
}
const data = await response.json();
// Get generated text from response or default to empty string (if null)
const output = data?.candidates?.[0]?.content?.parts?.[0]?.text || "";
let inputTranscriptName = path.basename(transcriptPath, path.extname(transcriptPath)); // Name for the output file
console.log(inputTranscriptName);
const outPath = path.join(outputDir, `${inputTranscriptName}.md`); // Output file path & name to make naming dynamic. Pulled from input transcript name
fs.writeFileSync(outPath, output, "utf8"); // Write output to file
console.log("Generated document written to:", outPath);
} catch (error) {
console.error("Error generating Gemini content:", error);
}
}
};
@@ -1,128 +1,8 @@
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}`);
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
}
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,7 +1,6 @@
// 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';
/**
+39
View File
@@ -0,0 +1,39 @@
**Goal:** Generate a structured meeting report (Markdown). **Output ONLY:** final .md. No meta.
```json
{
"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": "1 sentence",
"key_points": "<=5 bullets, quotes optional",
"decisions": "list: text | owner | due",
"actions": "table: id | task | owner | due | status"
},
"exec_summary": "3 short sentences",
"consolidated": ["decisions", "actions"],
"appendix": "optional"
},
"STYLE": {
"tone": "neutral, concise",
"ts_format": "HH:MM:SS",
"no_meta": true
},
"PROCESS": {
"timestamps": "use if present; else estimate minimal",
"speakers": "use labels; else Speaker X",
"long_transcripts": "chunk → summarize → merge",
"unclear": "UNKLAR:<reason>"
},
"JSON_OUTPUT_OPTIONAL": true,
"PROMPT_SNIPPET": "Generate meeting report in markdown using STRUCTURE and STYLE. Output only the report."
}
+7
View File
@@ -0,0 +1,7 @@
const gemini = require("../../../services/modules/llm-gemini/gemini.js");
gemini.function({
inputTranscriptPath: "./transcript.json",
documentTypePath: "./documentType.json",
language: "de"
});
+122
View File
@@ -0,0 +1,122 @@
[
{
"speaker": "A",
"sentence": "Smoke from hundreds of wildfires in Canada is triggering air quality alerts throughout the US Skylines from Maine to Maryland to Minnesota are gray and smoggy. And in some places, the air quality warnings include the warning to stay inside. We wanted to better understand what's happening here and why, so we called Peter DeCarlo, an associate professor in the Department of Environmental Health and Engineering at Johns Hopkins University. Good morning, Professor.",
"start": 240,
"end": 26560
},
{
"speaker": "B",
"sentence": "Good morning.",
"start": 28060,
"end": 28620
},
{
"speaker": "A",
"sentence": "So what is it about the conditions right now that have caused this round of wildfires to affect so many people so far away?",
"start": 29100,
"end": 37100
},
{
"speaker": "B",
"sentence": "Well, there's a couple of things. The season has been pretty dry already, and then the fact that we're getting hit in the US is because there's a couple weather systems that are essentially channeling the smoke from those Canadian wildfires through Pennsylvania into the mid Atlantic and the Northeast and kind of just dropping the smoke there.",
"start": 39100,
"end": 55820
},
{
"speaker": "A",
"sentence": "So what is it in this haze that makes it harmful? And I'm assuming it is harmful.",
"start": 56590,
"end": 60670
},
{
"speaker": "B",
"sentence": "It is, it is. The levels outside right now in Baltimore are considered unhealthy. And most of that is due to what's called particulate matter, which are tiny particles, microscopic, smaller than the width of your hair, that can get into your lungs and impact your respiratory system, your cardiovascular system, and even your neurological, your brain.",
"start": 62350,
"end": 82590
},
{
"speaker": "A",
"sentence": "What makes this particularly harmful? Is it the volume of particulate? Is it something in particular? What is it exactly? Can you just drill down on that a little bit more?",
"start": 83630,
"end": 92190
},
{
"speaker": "B",
"sentence": "Yeah. So the concentration of particulate matter, I was looking at some of the monitors that we have was reaching levels of what are, in science speak, 150 micrograms per meter cubed, which is more than 10 times what the annual average should be in about four times higher than what you're supposed to have on a 24 hour average. And so the concentrations of these particles in the air are just much, much, much higher than we typically see. And exposure to those high levels can lead to a host of health problems.",
"start": 93550,
"end": 123350
},
{
"speaker": "A",
"sentence": "And who is most vulnerable? I noticed that in New York City, for example, they're canceling outdoor activities. And so here it is in the early days of summer and they have to keep all the kids inside. So who tends to be vulnerable in a situation like this?",
"start": 123430,
"end": 135990
},
{
"speaker": "B",
"sentence": "It's the youngest. So children, obviously, whose bodies are still developing, the elderly who are, you know, their bodies are more in decline and they're more susceptible to the health impacts of breathing, the poor air quality. And then people who have pre existing health conditions, people with respiratory conditions or heart conditions, can be triggered by high levels of air pollution.",
"start": 137610,
"end": 156650
},
{
"speaker": "A",
"sentence": "Could this get worse?",
"start": 157450,
"end": 158650
},
{
"speaker": "B",
"sentence": "That's a good question. I mean, I think if in some areas it's much worse than others and it just depends on kind of where the smoke is concentrated. I think New York has some of the higher concentrations right now, but that's going to change as that air moves away from the New York area. But over the course of the next few days, we will see different areas being hit at different times with the highest concentrations.",
"start": 162170,
"end": 183420
},
{
"speaker": "A",
"sentence": "I was going to ask you about.",
"start": 183740,
"end": 184660
},
{
"speaker": "B",
"sentence": "More fires start burning. I don't expect the concentrations to go up too much higher.",
"start": 184660,
"end": 189020
},
{
"speaker": "A",
"sentence": "I was going to ask you how and you started to answer this, but how much longer could this last? Forgive me if I'm asking you to speculate, but what do you think?",
"start": 189100,
"end": 196220
},
{
"speaker": "B",
"sentence": "Well, I think the fires are going to burn for a little bit longer. But the key for us in the US Is the weather system changing. Right now it's the weather systems that are pulling that air into our Mid Atlantic and Northeast region. As those weather systems change and shift, we'll see that smoke going elsewhere and not impact us in this region as much. I think that's going to be the defining factor. I think the next couple days we're going to see a shift in that weather pattern and start to push the smoke away from where we are.",
"start": 198280,
"end": 227480
},
{
"speaker": "A",
"sentence": "And finally, with the impacts of climate change, we are seeing more wildfires. Will we be seeing more of these kinds of wide ranging air quality consequences or circumstances?",
"start": 227930,
"end": 239850
},
{
"speaker": "B",
"sentence": "I mean, that is one of the predictions for climate change. Looking into the future, the fire season is starting earlier and lasting longer and we're seeing more frequent fires. So yeah, this is probably something that we'll be seeing more, more frequently. This tends to be much more of an issue in the western U.S. so the eastern U.S. getting hit right now is a little bit new. But yeah, I think with climate change moving forward, this is something that is going to happen more frequently.",
"start": 241370,
"end": 267570
},
{
"speaker": "A",
"sentence": "That's Peter DeCarlo, associate professor in the Department of Environmental Health and Engineering at Johns Hopkins University. Professor DeCarlo, thanks so much for joining us and sharing this expertise with us.",
"start": 267970,
"end": 278210
},
{
"speaker": "B",
"sentence": "Thank you for having me.",
"start": 279410,
"end": 280530
}
]