add async content processing
This commit is contained in:
parent
ac2ea9e554
commit
00f070a644
10 changed files with 541 additions and 395 deletions
|
|
@ -1,4 +1,5 @@
|
||||||
import os
|
import os
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
import magic
|
import magic
|
||||||
from langgraph.graph import END, START, StateGraph
|
from langgraph.graph import END, START, StateGraph
|
||||||
|
|
@ -21,7 +22,7 @@ from open_notebook.graphs.content_processing.video import extract_best_audio_fro
|
||||||
from open_notebook.graphs.content_processing.youtube import extract_youtube_transcript
|
from open_notebook.graphs.content_processing.youtube import extract_youtube_transcript
|
||||||
|
|
||||||
|
|
||||||
def source_identification(state: ContentState):
|
async def source_identification(state: ContentState) -> Dict[str, str]:
|
||||||
"""
|
"""
|
||||||
Identify the content source based on parameters
|
Identify the content source based on parameters
|
||||||
"""
|
"""
|
||||||
|
|
@ -37,7 +38,7 @@ def source_identification(state: ContentState):
|
||||||
return {"source_type": doc_type}
|
return {"source_type": doc_type}
|
||||||
|
|
||||||
|
|
||||||
def file_type(state: ContentState):
|
async def file_type(state: ContentState) -> Dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Identify the file using python-magic
|
Identify the file using python-magic
|
||||||
"""
|
"""
|
||||||
|
|
@ -49,7 +50,7 @@ def file_type(state: ContentState):
|
||||||
return return_dict
|
return return_dict
|
||||||
|
|
||||||
|
|
||||||
def file_type_edge(data: ContentState):
|
async def file_type_edge(data: ContentState) -> str:
|
||||||
assert data.get("identified_type"), "Type not identified"
|
assert data.get("identified_type"), "Type not identified"
|
||||||
identified_type = data["identified_type"]
|
identified_type = data["identified_type"]
|
||||||
|
|
||||||
|
|
@ -69,7 +70,7 @@ def file_type_edge(data: ContentState):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def delete_file(data: ContentState):
|
async def delete_file(data: ContentState) -> Dict[str, Any]:
|
||||||
if data.get("delete_source"):
|
if data.get("delete_source"):
|
||||||
logger.debug(f"Deleting file: {data.get('file_path')}")
|
logger.debug(f"Deleting file: {data.get('file_path')}")
|
||||||
file_path = data.get("file_path")
|
file_path = data.get("file_path")
|
||||||
|
|
@ -81,9 +82,21 @@ def delete_file(data: ContentState):
|
||||||
logger.warning(f"File not found while trying to delete: {file_path}")
|
logger.warning(f"File not found while trying to delete: {file_path}")
|
||||||
else:
|
else:
|
||||||
logger.debug("Not deleting file")
|
logger.debug("Not deleting file")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
async def url_type_router(x: ContentState) -> str:
|
||||||
|
return x.get("identified_type", "")
|
||||||
|
|
||||||
|
|
||||||
|
async def source_type_router(x: ContentState) -> str:
|
||||||
|
return x.get("source_type", "")
|
||||||
|
|
||||||
|
|
||||||
|
# Create workflow
|
||||||
workflow = StateGraph(ContentState)
|
workflow = StateGraph(ContentState)
|
||||||
|
|
||||||
|
# Add nodes
|
||||||
workflow.add_node("source", source_identification)
|
workflow.add_node("source", source_identification)
|
||||||
workflow.add_node("url_provider", url_provider)
|
workflow.add_node("url_provider", url_provider)
|
||||||
workflow.add_node("file_type", file_type)
|
workflow.add_node("file_type", file_type)
|
||||||
|
|
@ -95,10 +108,12 @@ workflow.add_node("extract_best_audio_from_video", extract_best_audio_from_video
|
||||||
workflow.add_node("extract_audio", extract_audio)
|
workflow.add_node("extract_audio", extract_audio)
|
||||||
workflow.add_node("extract_youtube_transcript", extract_youtube_transcript)
|
workflow.add_node("extract_youtube_transcript", extract_youtube_transcript)
|
||||||
workflow.add_node("delete_file", delete_file)
|
workflow.add_node("delete_file", delete_file)
|
||||||
|
|
||||||
|
# Add edges
|
||||||
workflow.add_edge(START, "source")
|
workflow.add_edge(START, "source")
|
||||||
workflow.add_conditional_edges(
|
workflow.add_conditional_edges(
|
||||||
"source",
|
"source",
|
||||||
lambda x: x.get("source_type"),
|
source_type_router,
|
||||||
{
|
{
|
||||||
"url": "url_provider",
|
"url": "url_provider",
|
||||||
"file": "file_type",
|
"file": "file_type",
|
||||||
|
|
@ -111,7 +126,7 @@ workflow.add_conditional_edges(
|
||||||
)
|
)
|
||||||
workflow.add_conditional_edges(
|
workflow.add_conditional_edges(
|
||||||
"url_provider",
|
"url_provider",
|
||||||
lambda x: x.get("identified_type"),
|
url_type_router,
|
||||||
{"article": "extract_url", "youtube": "extract_youtube_transcript"},
|
{"article": "extract_url", "youtube": "extract_youtube_transcript"},
|
||||||
)
|
)
|
||||||
workflow.add_edge("url_provider", END)
|
workflow.add_edge("url_provider", END)
|
||||||
|
|
@ -125,4 +140,6 @@ workflow.add_edge("extract_office_content", "delete_file")
|
||||||
workflow.add_edge("extract_best_audio_from_video", "extract_audio")
|
workflow.add_edge("extract_best_audio_from_video", "extract_audio")
|
||||||
workflow.add_edge("extract_audio", "delete_file")
|
workflow.add_edge("extract_audio", "delete_file")
|
||||||
workflow.add_edge("delete_file", END)
|
workflow.add_edge("delete_file", END)
|
||||||
|
|
||||||
|
# Compile graph
|
||||||
graph = workflow.compile()
|
graph = workflow.compile()
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
|
import asyncio
|
||||||
import os
|
import os
|
||||||
|
from functools import partial
|
||||||
from math import ceil
|
from math import ceil
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
@ -11,45 +13,35 @@ from open_notebook.graphs.content_processing.state import ContentState
|
||||||
# future: parallelize the transcription process
|
# future: parallelize the transcription process
|
||||||
|
|
||||||
|
|
||||||
def split_audio(input_file, segment_length_minutes=15, output_prefix=None):
|
async def split_audio(input_file, segment_length_minutes=15, output_prefix=None):
|
||||||
"""
|
"""
|
||||||
Split an audio file into segments of specified length.
|
Split an audio file into segments asynchronously.
|
||||||
|
|
||||||
Args:
|
|
||||||
input_file (str): Path to the input audio file
|
|
||||||
segment_length_minutes (int): Length of each segment in minutes
|
|
||||||
output_dir (str): Directory to save the segments (defaults to input file's directory)
|
|
||||||
output_prefix (str): Prefix for output files (defaults to input filename)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list: List of paths to the created segment files
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def _split(input_file, segment_length_minutes, output_prefix):
|
||||||
# Convert input file to absolute path
|
# Convert input file to absolute path
|
||||||
input_file = os.path.abspath(input_file)
|
input_file_abs = os.path.abspath(input_file)
|
||||||
|
output_dir = os.path.dirname(input_file_abs)
|
||||||
output_dir = os.path.dirname(input_file)
|
|
||||||
os.makedirs(output_dir, exist_ok=True)
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
|
||||||
# Set up output prefix
|
# Set up output prefix
|
||||||
if output_prefix is None:
|
if output_prefix is None:
|
||||||
output_prefix = os.path.splitext(os.path.basename(input_file))[0]
|
output_prefix = os.path.splitext(os.path.basename(input_file_abs))[0]
|
||||||
|
|
||||||
# Load the audio file
|
# Load the audio file
|
||||||
audio = AudioSegment.from_file(input_file)
|
audio = AudioSegment.from_file(input_file_abs)
|
||||||
|
|
||||||
# Calculate segment length in milliseconds
|
# Calculate segment length in milliseconds
|
||||||
segment_length_ms = segment_length_minutes * 60 * 1000
|
segment_length_ms = segment_length_minutes * 60 * 1000
|
||||||
|
|
||||||
# Calculate number of segments
|
# Calculate number of segments
|
||||||
total_segments = ceil(len(audio) / segment_length_ms)
|
total_segments = ceil(len(audio) / segment_length_ms)
|
||||||
logger.debug(f"Splitting file: {input_file} into {total_segments} segments")
|
logger.debug(f"Splitting file: {input_file_abs} into {total_segments} segments")
|
||||||
|
|
||||||
# List to store output file paths
|
|
||||||
output_files = []
|
output_files = []
|
||||||
|
|
||||||
# Split the audio into segments
|
# Split the audio into segments
|
||||||
for i in range(total_segments):
|
for i in range(total_segments):
|
||||||
# Calculate start and end times for this segment
|
|
||||||
start_time = i * segment_length_ms
|
start_time = i * segment_length_ms
|
||||||
end_time = min((i + 1) * segment_length_ms, len(audio))
|
end_time = min((i + 1) * segment_length_ms, len(audio))
|
||||||
|
|
||||||
|
|
@ -57,44 +49,66 @@ def split_audio(input_file, segment_length_minutes=15, output_prefix=None):
|
||||||
segment = audio[start_time:end_time]
|
segment = audio[start_time:end_time]
|
||||||
|
|
||||||
# Generate output filename
|
# Generate output filename
|
||||||
# Format: prefix_001.mp3 (padding with zeros ensures correct ordering)
|
|
||||||
output_filename = f"{output_prefix}_{str(i+1).zfill(3)}.mp3"
|
output_filename = f"{output_prefix}_{str(i+1).zfill(3)}.mp3"
|
||||||
output_path = os.path.join(output_dir, output_filename)
|
output_path = os.path.join(output_dir, output_filename)
|
||||||
|
|
||||||
# Export segment
|
# Export segment
|
||||||
segment.export(output_path, format="mp3")
|
segment.export(output_path, format="mp3")
|
||||||
|
|
||||||
output_files.append(output_path)
|
output_files.append(output_path)
|
||||||
|
|
||||||
# Optional progress indication
|
|
||||||
logger.debug(f"Exported segment {i+1}/{total_segments}: {output_filename}")
|
logger.debug(f"Exported segment {i+1}/{total_segments}: {output_filename}")
|
||||||
|
|
||||||
return output_files
|
return output_files
|
||||||
|
|
||||||
|
# Run CPU-bound audio processing in thread pool
|
||||||
|
return await asyncio.get_event_loop().run_in_executor(
|
||||||
|
None, partial(_split, input_file, segment_length_minutes, output_prefix)
|
||||||
|
)
|
||||||
|
|
||||||
def extract_audio(data: ContentState):
|
|
||||||
|
async def transcribe_audio_segment(audio_file, model):
|
||||||
|
"""Transcribe a single audio segment asynchronously"""
|
||||||
|
|
||||||
|
def _transcribe(audio_file, model):
|
||||||
|
return model.transcribe(audio_file)
|
||||||
|
|
||||||
|
return await asyncio.get_event_loop().run_in_executor(
|
||||||
|
None, partial(_transcribe, audio_file, model)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def extract_audio(data: ContentState):
|
||||||
SPEECH_TO_TEXT_MODEL = model_manager.speech_to_text
|
SPEECH_TO_TEXT_MODEL = model_manager.speech_to_text
|
||||||
|
|
||||||
input_audio_path = data.get("file_path")
|
input_audio_path = data.get("file_path")
|
||||||
audio_files = []
|
audio_files = []
|
||||||
|
|
||||||
try:
|
try:
|
||||||
audio_files = split_audio(input_audio_path)
|
# Split audio into segments
|
||||||
transcriptions = []
|
audio_files = await split_audio(input_audio_path)
|
||||||
|
|
||||||
for audio_file in audio_files:
|
# Transcribe all segments concurrently
|
||||||
transcriptions.append(SPEECH_TO_TEXT_MODEL.transcribe(audio_file))
|
transcribe_tasks = [
|
||||||
|
transcribe_audio_segment(audio_file, SPEECH_TO_TEXT_MODEL)
|
||||||
|
for audio_file in audio_files
|
||||||
|
]
|
||||||
|
transcriptions = await asyncio.gather(*transcribe_tasks)
|
||||||
|
|
||||||
return {"content": " ".join(transcriptions)}
|
return {"content": " ".join(transcriptions)}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Error transcribing audio: {str(e)}")
|
logger.error(f"Error transcribing audio: {str(e)}")
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
raise # Re-raise the exception after logging
|
raise
|
||||||
|
|
||||||
finally:
|
finally:
|
||||||
for file in audio_files:
|
# Clean up temporary files
|
||||||
|
def _cleanup(files):
|
||||||
|
for file in files:
|
||||||
try:
|
try:
|
||||||
os.remove(file)
|
os.remove(file)
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
logger.error(f"Error removing temporary file {file}: {str(e)}")
|
logger.error(f"Error removing temporary file {file}: {str(e)}")
|
||||||
|
|
||||||
|
await asyncio.get_event_loop().run_in_executor(
|
||||||
|
None, partial(_cleanup, audio_files)
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
import asyncio
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
from docx import Document
|
from docx import Document
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from openpyxl import load_workbook
|
from openpyxl import load_workbook
|
||||||
|
|
@ -12,7 +15,10 @@ SUPPORTED_OFFICE_TYPES = [
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def extract_docx_content_detailed(file_path):
|
async def extract_docx_content_detailed(file_path):
|
||||||
|
"""Extract content from DOCX file"""
|
||||||
|
|
||||||
|
def _extract():
|
||||||
try:
|
try:
|
||||||
doc = Document(file_path)
|
doc = Document(file_path)
|
||||||
content = []
|
content = []
|
||||||
|
|
@ -89,9 +95,13 @@ def extract_docx_content_detailed(file_path):
|
||||||
logger.error(f"Failed to extract DOCX content: {e}")
|
logger.error(f"Failed to extract DOCX content: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
return await asyncio.get_event_loop().run_in_executor(None, _extract)
|
||||||
|
|
||||||
# Example of usage with metadata
|
|
||||||
def get_docx_info(file_path):
|
async def get_docx_info(file_path):
|
||||||
|
"""Get DOCX metadata and content"""
|
||||||
|
|
||||||
|
def _get_info():
|
||||||
try:
|
try:
|
||||||
doc = Document(file_path)
|
doc = Document(file_path)
|
||||||
|
|
||||||
|
|
@ -127,8 +137,13 @@ def get_docx_info(file_path):
|
||||||
logger.error(f"Failed to get DOCX info: {e}")
|
logger.error(f"Failed to get DOCX info: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
return await asyncio.get_event_loop().run_in_executor(None, _get_info)
|
||||||
|
|
||||||
def extract_pptx_content(file_path):
|
|
||||||
|
async def extract_pptx_content(file_path):
|
||||||
|
"""Extract content from PPTX file"""
|
||||||
|
|
||||||
|
def _extract():
|
||||||
try:
|
try:
|
||||||
prs = Presentation(file_path)
|
prs = Presentation(file_path)
|
||||||
content = []
|
content = []
|
||||||
|
|
@ -143,7 +158,9 @@ def extract_pptx_content(file_path):
|
||||||
# Extract text from all shapes
|
# Extract text from all shapes
|
||||||
for shape in slide.shapes:
|
for shape in slide.shapes:
|
||||||
if hasattr(shape, "text") and shape.text.strip():
|
if hasattr(shape, "text") and shape.text.strip():
|
||||||
if shape != slide.shapes.title: # Skip title as it's already added
|
if (
|
||||||
|
shape != slide.shapes.title
|
||||||
|
): # Skip title as it's already added
|
||||||
content.append(shape.text.strip())
|
content.append(shape.text.strip())
|
||||||
|
|
||||||
return "\n\n".join(content)
|
return "\n\n".join(content)
|
||||||
|
|
@ -152,8 +169,13 @@ def extract_pptx_content(file_path):
|
||||||
logger.error(f"Failed to extract PPTX content: {e}")
|
logger.error(f"Failed to extract PPTX content: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
return await asyncio.get_event_loop().run_in_executor(None, _extract)
|
||||||
|
|
||||||
def extract_xlsx_content(file_path, max_rows=1000, max_cols=100):
|
|
||||||
|
async def extract_xlsx_content(file_path, max_rows=10000, max_cols=100):
|
||||||
|
"""Extract content from XLSX file"""
|
||||||
|
|
||||||
|
def _extract():
|
||||||
try:
|
try:
|
||||||
wb = load_workbook(file_path, data_only=True)
|
wb = load_workbook(file_path, data_only=True)
|
||||||
content = []
|
content = []
|
||||||
|
|
@ -180,7 +202,9 @@ def extract_xlsx_content(file_path, max_rows=1000, max_cols=100):
|
||||||
row_data = []
|
row_data = []
|
||||||
for col in range(1, max_col + 1):
|
for col in range(1, max_col + 1):
|
||||||
cell_value = ws.cell(row=row, column=col).value
|
cell_value = ws.cell(row=row, column=col).value
|
||||||
row_data.append(str(cell_value) if cell_value is not None else "")
|
row_data.append(
|
||||||
|
str(cell_value) if cell_value is not None else ""
|
||||||
|
)
|
||||||
content.append("| " + " | ".join(row_data) + " |")
|
content.append("| " + " | ".join(row_data) + " |")
|
||||||
|
|
||||||
return "\n".join(content)
|
return "\n".join(content)
|
||||||
|
|
@ -189,8 +213,13 @@ def extract_xlsx_content(file_path, max_rows=1000, max_cols=100):
|
||||||
logger.error(f"Failed to extract XLSX content: {e}")
|
logger.error(f"Failed to extract XLSX content: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
return await asyncio.get_event_loop().run_in_executor(None, partial(_extract))
|
||||||
|
|
||||||
def get_pptx_info(file_path):
|
|
||||||
|
async def get_pptx_info(file_path):
|
||||||
|
"""Get PPTX metadata and content"""
|
||||||
|
|
||||||
|
def _get_info():
|
||||||
try:
|
try:
|
||||||
prs = Presentation(file_path)
|
prs = Presentation(file_path)
|
||||||
|
|
||||||
|
|
@ -219,8 +248,13 @@ def get_pptx_info(file_path):
|
||||||
logger.error(f"Failed to get PPTX info: {e}")
|
logger.error(f"Failed to get PPTX info: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
return await asyncio.get_event_loop().run_in_executor(None, _get_info)
|
||||||
|
|
||||||
def get_xlsx_info(file_path):
|
|
||||||
|
async def get_xlsx_info(file_path):
|
||||||
|
"""Get XLSX metadata and content"""
|
||||||
|
|
||||||
|
def _get_info():
|
||||||
try:
|
try:
|
||||||
wb = load_workbook(file_path, data_only=True)
|
wb = load_workbook(file_path, data_only=True)
|
||||||
|
|
||||||
|
|
@ -250,14 +284,15 @@ def get_xlsx_info(file_path):
|
||||||
logger.error(f"Failed to get XLSX info: {e}")
|
logger.error(f"Failed to get XLSX info: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
return await asyncio.get_event_loop().run_in_executor(None, _get_info)
|
||||||
|
|
||||||
def extract_office_content(state: ContentState):
|
|
||||||
|
async def extract_office_content(state: ContentState):
|
||||||
"""Universal function to extract content from Office files"""
|
"""Universal function to extract content from Office files"""
|
||||||
assert state.get("file_path"), "No file path provided"
|
assert state.get("file_path"), "No file path provided"
|
||||||
assert (
|
assert (
|
||||||
state.get("identified_type") in SUPPORTED_OFFICE_TYPES
|
state.get("identified_type") in SUPPORTED_OFFICE_TYPES
|
||||||
), "Unsupported File Type"
|
), "Unsupported File Type"
|
||||||
|
|
||||||
file_path = state["file_path"]
|
file_path = state["file_path"]
|
||||||
doc_type = state["identified_type"]
|
doc_type = state["identified_type"]
|
||||||
|
|
||||||
|
|
@ -266,24 +301,23 @@ def extract_office_content(state: ContentState):
|
||||||
== "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
== "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
|
||||||
):
|
):
|
||||||
logger.debug("Extracting content from DOCX file")
|
logger.debug("Extracting content from DOCX file")
|
||||||
content = extract_docx_content_detailed(file_path)
|
content = await extract_docx_content_detailed(file_path)
|
||||||
info = get_docx_info(file_path)
|
info = await get_docx_info(file_path)
|
||||||
elif (
|
elif (
|
||||||
doc_type
|
doc_type
|
||||||
== "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
== "application/vnd.openxmlformats-officedocument.presentationml.presentation"
|
||||||
):
|
):
|
||||||
logger.debug("Extracting content from PPTX file")
|
logger.debug("Extracting content from PPTX file")
|
||||||
content = extract_pptx_content(file_path)
|
content = await extract_pptx_content(file_path)
|
||||||
info = get_pptx_info(file_path)
|
info = await get_pptx_info(file_path)
|
||||||
elif (
|
elif (
|
||||||
doc_type == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
doc_type == "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||||
):
|
):
|
||||||
logger.debug("Extracting content from XLSX file")
|
logger.debug("Extracting content from XLSX file")
|
||||||
content = extract_xlsx_content(file_path)
|
content = await extract_xlsx_content(file_path)
|
||||||
info = get_xlsx_info(file_path)
|
info = await get_xlsx_info(file_path)
|
||||||
else:
|
else:
|
||||||
raise Exception(f"Unsupported file format: {doc_type}")
|
raise Exception(f"Unsupported file format: {doc_type}")
|
||||||
|
|
||||||
del info["content"]
|
del info["content"]
|
||||||
|
|
||||||
return {"content": content, "metadata": info}
|
return {"content": content, "metadata": info}
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import asyncio
|
||||||
import re
|
import re
|
||||||
import unicodedata
|
import unicodedata
|
||||||
|
|
||||||
|
|
@ -114,7 +115,7 @@ def clean_pdf_text(text):
|
||||||
return text.strip()
|
return text.strip()
|
||||||
|
|
||||||
|
|
||||||
def _extract_text_from_pdf(pdf_path):
|
async def _extract_text_from_pdf(pdf_path):
|
||||||
doc = fitz.open(pdf_path)
|
doc = fitz.open(pdf_path)
|
||||||
try:
|
try:
|
||||||
text = ""
|
text = ""
|
||||||
|
|
@ -127,20 +128,39 @@ def _extract_text_from_pdf(pdf_path):
|
||||||
doc.close()
|
doc.close()
|
||||||
|
|
||||||
|
|
||||||
def extract_pdf(state: ContentState):
|
async def _extract_text_from_pdf(pdf_path):
|
||||||
|
"""Extract text from PDF asynchronously"""
|
||||||
|
|
||||||
|
def _extract():
|
||||||
|
doc = fitz.open(pdf_path)
|
||||||
|
try:
|
||||||
|
text = ""
|
||||||
|
logger.debug(f"Found {len(doc)} pages in PDF")
|
||||||
|
for page in doc:
|
||||||
|
text += page.get_text()
|
||||||
|
return clean_pdf_text(text)
|
||||||
|
finally:
|
||||||
|
doc.close()
|
||||||
|
|
||||||
|
# Run CPU-bound PDF processing in a thread pool
|
||||||
|
return await asyncio.get_event_loop().run_in_executor(None, _extract)
|
||||||
|
|
||||||
|
|
||||||
|
async def extract_pdf(state: ContentState):
|
||||||
"""
|
"""
|
||||||
Parse the text file and print its content.
|
Parse the PDF file and extract its content asynchronously.
|
||||||
"""
|
"""
|
||||||
return_dict = {}
|
return_dict = {}
|
||||||
assert state.get("file_path"), "No file path provided"
|
assert state.get("file_path"), "No file path provided"
|
||||||
assert state.get("identified_type") in SUPPORTED_FITZ_TYPES, "Unsupported File Type"
|
assert state.get("identified_type") in SUPPORTED_FITZ_TYPES, "Unsupported File Type"
|
||||||
|
|
||||||
if (
|
if (
|
||||||
state.get("file_path") is not None
|
state.get("file_path") is not None
|
||||||
and state.get("identified_type") in SUPPORTED_FITZ_TYPES
|
and state.get("identified_type") in SUPPORTED_FITZ_TYPES
|
||||||
):
|
):
|
||||||
file_path = state.get("file_path")
|
file_path = state.get("file_path")
|
||||||
try:
|
try:
|
||||||
text = _extract_text_from_pdf(file_path)
|
text = await _extract_text_from_pdf(file_path)
|
||||||
return_dict["content"] = text
|
return_dict["content"] = text
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise FileNotFoundError(f"File not found at {file_path}")
|
raise FileNotFoundError(f"File not found at {file_path}")
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
|
import asyncio
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from open_notebook.graphs.content_processing.state import ContentState
|
from open_notebook.graphs.content_processing.state import ContentState
|
||||||
|
|
||||||
|
|
||||||
def extract_txt(state: ContentState):
|
async def extract_txt(state: ContentState):
|
||||||
"""
|
"""
|
||||||
Parse the text file and print its content.
|
Parse the text file and extract its content asynchronously.
|
||||||
"""
|
"""
|
||||||
return_dict = {}
|
return_dict = {}
|
||||||
if (
|
if (
|
||||||
|
|
@ -14,12 +16,22 @@ def extract_txt(state: ContentState):
|
||||||
):
|
):
|
||||||
logger.debug(f"Extracting text from {state.get('file_path')}")
|
logger.debug(f"Extracting text from {state.get('file_path')}")
|
||||||
file_path = state.get("file_path")
|
file_path = state.get("file_path")
|
||||||
|
|
||||||
if file_path is not None:
|
if file_path is not None:
|
||||||
try:
|
try:
|
||||||
|
|
||||||
|
def _read_file():
|
||||||
with open(file_path, "r", encoding="utf-8") as file:
|
with open(file_path, "r", encoding="utf-8") as file:
|
||||||
content = file.read()
|
return file.read()
|
||||||
|
|
||||||
|
# Run file I/O in thread pool
|
||||||
|
content = await asyncio.get_event_loop().run_in_executor(
|
||||||
|
None, _read_file
|
||||||
|
)
|
||||||
|
|
||||||
logger.debug(f"Extracted: {content[:100]}")
|
logger.debug(f"Extracted: {content[:100]}")
|
||||||
return_dict["content"] = content
|
return_dict["content"] = content
|
||||||
|
|
||||||
except FileNotFoundError:
|
except FileNotFoundError:
|
||||||
raise FileNotFoundError(f"File not found at {file_path}")
|
raise FileNotFoundError(f"File not found at {file_path}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import re
|
import re
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
import requests # type: ignore
|
import aiohttp
|
||||||
from bs4 import BeautifulSoup, Comment
|
from bs4 import BeautifulSoup, Comment
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
|
|
@ -29,7 +29,7 @@ def url_provider(state: ContentState):
|
||||||
return return_dict
|
return return_dict
|
||||||
|
|
||||||
|
|
||||||
def extract_url_bs4(url: str):
|
async def extract_url_bs4(url: str):
|
||||||
"""
|
"""
|
||||||
Get the title and content of a URL using bs4
|
Get the title and content of a URL using bs4
|
||||||
"""
|
"""
|
||||||
|
|
@ -42,9 +42,10 @@ def extract_url_bs4(url: str):
|
||||||
if url.startswith("<!DOCTYPE html>") or url.startswith("<html"):
|
if url.startswith("<!DOCTYPE html>") or url.startswith("<html"):
|
||||||
html_content = url
|
html_content = url
|
||||||
else:
|
else:
|
||||||
response = requests.get(url, headers=headers, timeout=10)
|
async with aiohttp.ClientSession() as session:
|
||||||
|
async with session.get(url, headers=headers, timeout=10) as response:
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
html_content = response.text
|
html_content = await response.text()
|
||||||
|
|
||||||
soup = BeautifulSoup(html_content, "html.parser")
|
soup = BeautifulSoup(html_content, "html.parser")
|
||||||
|
|
||||||
|
|
@ -143,7 +144,7 @@ def extract_url_bs4(url: str):
|
||||||
"url": url if not url.startswith("<!DOCTYPE html>") else None,
|
"url": url if not url.startswith("<!DOCTYPE html>") else None,
|
||||||
}
|
}
|
||||||
|
|
||||||
except requests.exceptions.RequestException as e:
|
except aiohttp.ClientError as e:
|
||||||
logger.error(f"Failed to fetch URL {url}: {e}")
|
logger.error(f"Failed to fetch URL {url}: {e}")
|
||||||
return None
|
return None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -151,12 +152,13 @@ def extract_url_bs4(url: str):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def extract_url_jina(url: str):
|
async def extract_url_jina(url: str):
|
||||||
"""
|
"""
|
||||||
Get the content of a URL using Jina
|
Get the content of a URL using Jina
|
||||||
"""
|
"""
|
||||||
response = requests.get(f"https://r.jina.ai/{url}")
|
async with aiohttp.ClientSession() as session:
|
||||||
text = response.text
|
async with session.get(f"https://r.jina.ai/{url}") as response:
|
||||||
|
text = await response.text()
|
||||||
if text.startswith("Title:") and "\n" in text:
|
if text.startswith("Title:") and "\n" in text:
|
||||||
title_end = text.index("\n")
|
title_end = text.index("\n")
|
||||||
title = text[6:title_end].strip()
|
title = text[6:title_end].strip()
|
||||||
|
|
@ -166,23 +168,22 @@ def extract_url_jina(url: str):
|
||||||
)
|
)
|
||||||
return {"title": title, "content": content}
|
return {"title": title, "content": content}
|
||||||
else:
|
else:
|
||||||
content = text
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"Processed url: {url}, does not have Title prefix, returning full content: {content[:100]}..."
|
f"Processed url: {url}, does not have Title prefix, returning full content: {text[:100]}..."
|
||||||
)
|
)
|
||||||
return {"content": text}
|
return {"content": text}
|
||||||
|
|
||||||
|
|
||||||
def extract_url(state: ContentState):
|
async def extract_url(state: ContentState):
|
||||||
assert state.get("url"), "No URL provided"
|
assert state.get("url"), "No URL provided"
|
||||||
url = state["url"]
|
url = state["url"]
|
||||||
try:
|
try:
|
||||||
result = extract_url_bs4(url)
|
result = await extract_url_bs4(url)
|
||||||
if not result or not result.get("content"):
|
if not result or not result.get("content"):
|
||||||
logger.debug(
|
logger.debug(
|
||||||
f"BS4 extraction failed for url {url}, falling back to Jina extractor"
|
f"BS4 extraction failed for url {url}, falling back to Jina extractor"
|
||||||
)
|
)
|
||||||
result = extract_url_jina(url)
|
result = await extract_url_jina(url)
|
||||||
return result
|
return result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"URL extraction failed for URL: {url}")
|
logger.error(f"URL extraction failed for URL: {url}")
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,20 @@
|
||||||
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from open_notebook.graphs.content_processing.state import ContentState
|
from open_notebook.graphs.content_processing.state import ContentState
|
||||||
|
|
||||||
|
|
||||||
def extract_audio_from_video(input_file, output_file, stream_index):
|
async def extract_audio_from_video(input_file, output_file, stream_index):
|
||||||
"""
|
"""
|
||||||
Extract the specified audio stream to MP3 format
|
Extract the specified audio stream to MP3 format asynchronously
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def _extract(input_file, output_file, stream_index):
|
||||||
try:
|
try:
|
||||||
cmd = [
|
cmd = [
|
||||||
"ffmpeg",
|
"ffmpeg",
|
||||||
|
|
@ -33,17 +37,22 @@ def extract_audio_from_video(input_file, output_file, stream_index):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error extracting audio: {str(e)}")
|
logger.error(f"Error extracting audio: {str(e)}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
return await asyncio.get_event_loop().run_in_executor(
|
||||||
|
None, partial(_extract, input_file, output_file, stream_index)
|
||||||
|
)
|
||||||
|
|
||||||
def get_audio_streams(input_file):
|
|
||||||
|
async def get_audio_streams(input_file):
|
||||||
"""
|
"""
|
||||||
Analyze video file and return information about all audio streams
|
Analyze video file and return information about all audio streams asynchronously
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def _analyze(input_file):
|
||||||
logger.debug(f"Analyzing video file {input_file} for audio streams")
|
logger.debug(f"Analyzing video file {input_file} for audio streams")
|
||||||
try:
|
try:
|
||||||
# Get stream information in JSON format
|
|
||||||
cmd = [
|
cmd = [
|
||||||
"ffprobe",
|
"ffprobe",
|
||||||
"-v",
|
"-v",
|
||||||
|
|
@ -64,14 +73,20 @@ def get_audio_streams(input_file):
|
||||||
return data.get("streams", [])
|
return data.get("streams", [])
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error analyzing file: {str(e)}")
|
logger.error(f"Error analyzing file: {str(e)}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
return await asyncio.get_event_loop().run_in_executor(
|
||||||
|
None, partial(_analyze, input_file)
|
||||||
|
)
|
||||||
|
|
||||||
def select_best_audio_stream(streams):
|
|
||||||
|
async def select_best_audio_stream(streams):
|
||||||
"""
|
"""
|
||||||
Select the best audio stream based on various quality metrics
|
Select the best audio stream based on various quality metrics
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def _select(streams):
|
||||||
if not streams:
|
if not streams:
|
||||||
logger.debug("No audio streams found")
|
logger.debug("No audio streams found")
|
||||||
return None
|
return None
|
||||||
|
|
@ -101,14 +116,26 @@ def select_best_audio_stream(streams):
|
||||||
# Return the stream with highest score
|
# Return the stream with highest score
|
||||||
return max(scored_streams, key=lambda x: x[0])[1]
|
return max(scored_streams, key=lambda x: x[0])[1]
|
||||||
|
|
||||||
|
return await asyncio.get_event_loop().run_in_executor(
|
||||||
|
None, partial(_select, streams)
|
||||||
|
)
|
||||||
|
|
||||||
def extract_best_audio_from_video(data: ContentState):
|
|
||||||
|
async def extract_best_audio_from_video(data: ContentState):
|
||||||
"""
|
"""
|
||||||
Main function to extract the best audio stream from a video file
|
Main function to extract the best audio stream from a video file asynchronously
|
||||||
"""
|
"""
|
||||||
input_file = data.get("file_path")
|
input_file = data.get("file_path")
|
||||||
assert input_file is not None, "Input file path must be provided"
|
assert input_file is not None, "Input file path must be provided"
|
||||||
if not os.path.exists(input_file):
|
|
||||||
|
def _check_file(path):
|
||||||
|
return os.path.exists(path)
|
||||||
|
|
||||||
|
file_exists = await asyncio.get_event_loop().run_in_executor(
|
||||||
|
None, partial(_check_file, input_file)
|
||||||
|
)
|
||||||
|
|
||||||
|
if not file_exists:
|
||||||
logger.critical(f"Input file not found: {input_file}")
|
logger.critical(f"Input file not found: {input_file}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
@ -116,20 +143,20 @@ def extract_best_audio_from_video(data: ContentState):
|
||||||
output_file = f"{base_name}_audio.mp3"
|
output_file = f"{base_name}_audio.mp3"
|
||||||
|
|
||||||
# Get all audio streams
|
# Get all audio streams
|
||||||
streams = get_audio_streams(input_file)
|
streams = await get_audio_streams(input_file)
|
||||||
if not streams:
|
if not streams:
|
||||||
logger.debug("No audio streams found in the file")
|
logger.debug("No audio streams found in the file")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Select best stream
|
# Select best stream
|
||||||
best_stream = select_best_audio_stream(streams)
|
best_stream = await select_best_audio_stream(streams)
|
||||||
if not best_stream:
|
if not best_stream:
|
||||||
logger.error("Could not determine best audio stream")
|
logger.error("Could not determine best audio stream")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Extract the selected stream
|
# Extract the selected stream
|
||||||
stream_index = streams.index(best_stream)
|
stream_index = streams.index(best_stream)
|
||||||
success = extract_audio_from_video(input_file, output_file, stream_index)
|
success = await extract_audio_from_video(input_file, output_file, stream_index)
|
||||||
|
|
||||||
if success:
|
if success:
|
||||||
logger.debug(f"Successfully extracted audio to: {output_file}")
|
logger.debug(f"Successfully extracted audio to: {output_file}")
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import re
|
import re
|
||||||
import ssl
|
import ssl
|
||||||
|
|
||||||
import requests
|
import aiohttp
|
||||||
from bs4 import BeautifulSoup
|
from bs4 import BeautifulSoup
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
from youtube_transcript_api import YouTubeTranscriptApi # type: ignore
|
from youtube_transcript_api import YouTubeTranscriptApi # type: ignore
|
||||||
|
|
@ -14,11 +14,15 @@ from open_notebook.graphs.content_processing.state import ContentState
|
||||||
ssl._create_default_https_context = ssl._create_unverified_context
|
ssl._create_default_https_context = ssl._create_unverified_context
|
||||||
|
|
||||||
|
|
||||||
def get_video_title(video_id):
|
async def get_video_title(video_id):
|
||||||
try:
|
try:
|
||||||
url = f"https://www.youtube.com/watch?v={video_id}"
|
url = f"https://www.youtube.com/watch?v={video_id}"
|
||||||
response = requests.get(url)
|
async with aiohttp.ClientSession() as session:
|
||||||
soup = BeautifulSoup(response.text, "html.parser")
|
async with session.get(url) as response:
|
||||||
|
html = await response.text()
|
||||||
|
|
||||||
|
# BeautifulSoup doesn't support async operations
|
||||||
|
soup = BeautifulSoup(html, "html.parser")
|
||||||
|
|
||||||
# YouTube stores title in a meta tag
|
# YouTube stores title in a meta tag
|
||||||
title = soup.find("meta", property="og:title")["content"]
|
title = soup.find("meta", property="og:title")["content"]
|
||||||
|
|
@ -63,7 +67,7 @@ def _extract_youtube_id(url):
|
||||||
return match.group(1) if match else None
|
return match.group(1) if match else None
|
||||||
|
|
||||||
|
|
||||||
def get_best_transcript(video_id, preferred_langs=["en", "es", "pt"]):
|
async def get_best_transcript(video_id, preferred_langs=["en", "es", "pt"]):
|
||||||
try:
|
try:
|
||||||
transcript_list = YouTubeTranscriptApi.list_transcripts(video_id)
|
transcript_list = YouTubeTranscriptApi.list_transcripts(video_id)
|
||||||
|
|
||||||
|
|
@ -129,7 +133,7 @@ def get_best_transcript(video_id, preferred_langs=["en", "es", "pt"]):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def extract_youtube_transcript(state: ContentState):
|
async def extract_youtube_transcript(state: ContentState):
|
||||||
"""
|
"""
|
||||||
Parse the text file and print its content.
|
Parse the text file and print its content.
|
||||||
"""
|
"""
|
||||||
|
|
@ -139,12 +143,12 @@ def extract_youtube_transcript(state: ContentState):
|
||||||
)
|
)
|
||||||
|
|
||||||
video_id = _extract_youtube_id(state.get("url"))
|
video_id = _extract_youtube_id(state.get("url"))
|
||||||
transcript = get_best_transcript(video_id, languages)
|
transcript = await get_best_transcript(video_id, languages)
|
||||||
|
|
||||||
logger.debug(f"Found transcript: {transcript}")
|
logger.debug(f"Found transcript: {transcript}")
|
||||||
formatter = TextFormatter()
|
formatter = TextFormatter()
|
||||||
try:
|
try:
|
||||||
title = get_video_title(video_id)
|
title = await get_video_title(video_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.critical(f"Failed to get video title for video_id: {video_id}")
|
logger.critical(f"Failed to get video title for video_id: {video_id}")
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,6 @@ from open_notebook.graphs.content_processing import graph as content_graph
|
||||||
from open_notebook.graphs.multipattern import graph as transform_graph
|
from open_notebook.graphs.multipattern import graph as transform_graph
|
||||||
from open_notebook.utils import surreal_clean
|
from open_notebook.utils import surreal_clean
|
||||||
|
|
||||||
# todo: we can make this more efficient
|
|
||||||
|
|
||||||
|
|
||||||
class SourceState(TypedDict):
|
class SourceState(TypedDict):
|
||||||
content_state: ContentState
|
content_state: ContentState
|
||||||
|
|
@ -32,20 +30,24 @@ class TransformationState(TypedDict):
|
||||||
transformation: dict
|
transformation: dict
|
||||||
|
|
||||||
|
|
||||||
def content_process(state: SourceState):
|
async def content_process(state: SourceState) -> dict:
|
||||||
content_state = state["content_state"]
|
content_state = state["content_state"]
|
||||||
logger.debug("Content processing started for new content")
|
logger.debug("Content processing started for new content")
|
||||||
return {"content_state": content_graph.invoke(content_state)}
|
processed_state = await content_graph.ainvoke(content_state)
|
||||||
|
return {"content_state": processed_state}
|
||||||
|
|
||||||
|
|
||||||
def run_patterns(input_text, patterns):
|
async def run_patterns(input_text: str, patterns: List[dict]) -> str:
|
||||||
output = transform_graph.invoke(dict(content_stack=[input_text], patterns=patterns))
|
output = await transform_graph.ainvoke(
|
||||||
|
dict(content_stack=[input_text], patterns=patterns)
|
||||||
|
)
|
||||||
return output["output"]
|
return output["output"]
|
||||||
|
|
||||||
|
|
||||||
def save_source(state: SourceState):
|
def save_source(state: SourceState) -> dict:
|
||||||
logger.debug("Saving source")
|
logger.debug("Saving source")
|
||||||
content_state = state["content_state"]
|
content_state = state["content_state"]
|
||||||
|
|
||||||
source = Source(
|
source = Source(
|
||||||
asset=Asset(
|
asset=Asset(
|
||||||
url=content_state.get("url"), file_path=content_state.get("file_path")
|
url=content_state.get("url"), file_path=content_state.get("file_path")
|
||||||
|
|
@ -61,9 +63,10 @@ def save_source(state: SourceState):
|
||||||
return {"source": source}
|
return {"source": source}
|
||||||
|
|
||||||
|
|
||||||
def trigger_transformations(state: SourceState, config: RunnableConfig):
|
def trigger_transformations(state: SourceState, config: RunnableConfig) -> List[Send]:
|
||||||
if len(state["transformations"]) == 0:
|
if len(state["transformations"]) == 0:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
transformations = Transformation.get_all()
|
transformations = Transformation.get_all()
|
||||||
to_apply = [
|
to_apply = [
|
||||||
t
|
t
|
||||||
|
|
@ -71,6 +74,7 @@ def trigger_transformations(state: SourceState, config: RunnableConfig):
|
||||||
if t["name"] in state["transformations"]
|
if t["name"] in state["transformations"]
|
||||||
]
|
]
|
||||||
logger.debug(f"Applying transformations {to_apply}")
|
logger.debug(f"Applying transformations {to_apply}")
|
||||||
|
|
||||||
return [
|
return [
|
||||||
Send(
|
Send(
|
||||||
"transform_content",
|
"transform_content",
|
||||||
|
|
@ -83,24 +87,34 @@ def trigger_transformations(state: SourceState, config: RunnableConfig):
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
def transform_content(state: TransformationState):
|
async def transform_content(state: TransformationState) -> dict:
|
||||||
source = state["source"]
|
source = state["source"]
|
||||||
content = source.full_text
|
content = source.full_text
|
||||||
transformation = state["transformation"]
|
transformation = state["transformation"]
|
||||||
|
|
||||||
logger.debug(f"Applying transformation {transformation['name']}")
|
logger.debug(f"Applying transformation {transformation['name']}")
|
||||||
result = run_patterns(content, patterns=transformation["patterns"])
|
result = await run_patterns(content, patterns=transformation["patterns"])
|
||||||
|
|
||||||
source.add_insight(transformation["name"], surreal_clean(result))
|
source.add_insight(transformation["name"], surreal_clean(result))
|
||||||
|
|
||||||
return {"transformations": [{"name": transformation["name"], "content": result}]}
|
return {"transformations": [{"name": transformation["name"], "content": result}]}
|
||||||
|
|
||||||
|
|
||||||
|
# Create and compile the workflow
|
||||||
workflow = StateGraph(SourceState)
|
workflow = StateGraph(SourceState)
|
||||||
|
|
||||||
|
# Add nodes
|
||||||
workflow.add_node("content_process", content_process)
|
workflow.add_node("content_process", content_process)
|
||||||
workflow.add_node("save_source", save_source)
|
workflow.add_node("save_source", save_source)
|
||||||
workflow.add_node("transform_content", transform_content)
|
workflow.add_node("transform_content", transform_content)
|
||||||
|
|
||||||
|
# Define the graph edges
|
||||||
workflow.add_edge(START, "content_process")
|
workflow.add_edge(START, "content_process")
|
||||||
workflow.add_edge("content_process", "save_source")
|
workflow.add_edge("content_process", "save_source")
|
||||||
workflow.add_conditional_edges(
|
workflow.add_conditional_edges(
|
||||||
"save_source", trigger_transformations, ["transform_content"]
|
"save_source", trigger_transformations, ["transform_content"]
|
||||||
)
|
)
|
||||||
workflow.add_edge("transform_content", END)
|
workflow.add_edge("transform_content", END)
|
||||||
|
|
||||||
|
# Compile the graph
|
||||||
source_graph = workflow.compile()
|
source_graph = workflow.compile()
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import asyncio
|
||||||
import os
|
import os
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
@ -71,13 +72,15 @@ def add_source(notebook_id):
|
||||||
f.write(source_file.getbuffer())
|
f.write(source_file.getbuffer())
|
||||||
|
|
||||||
st.write("Processing content..")
|
st.write("Processing content..")
|
||||||
source_graph.invoke(
|
asyncio.run(
|
||||||
|
source_graph.ainvoke(
|
||||||
{
|
{
|
||||||
"content_state": req,
|
"content_state": req,
|
||||||
"notebook_id": notebook_id,
|
"notebook_id": notebook_id,
|
||||||
"transformations": apply_transformations,
|
"transformations": apply_transformations,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
)
|
||||||
except UnsupportedTypeException as e:
|
except UnsupportedTypeException as e:
|
||||||
st.warning(
|
st.warning(
|
||||||
"This type of content is not supported yet. If you think it should be, let us know on the project Issues's page"
|
"This type of content is not supported yet. If you think it should be, let us know on the project Issues's page"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue