Data

Utilities for ingesting and working with local data.

load

extension_map = {'PNG': 'images', 'JPG': 'images', 'JPEG': 'images', 'GIF': 'images', 'SVG': 'images', 'WEBP': 'images', 'BMP': 'images', 'TIFF': 'images', 'MP4': 'videos', 'AVI': 'videos', 'MOV': 'videos', 'WMV': 'videos', 'MPG': 'videos', 'MPEG': 'videos', 'WEBM': 'videos', 'MKV': 'videos', 'DOCX': 'documents', 'PPTX': 'documents', 'PDF': 'documents', 'XLSX': 'documents', 'TXT': 'documents', 'CSV': 'documents', 'MD': 'documents', 'HTML': 'documents', 'HTM': 'documents', 'MP3': 'audio', 'WAV': 'audio', 'M4A': 'audio', 'AAC': 'audio', 'FLAC': 'audio', 'OGG': 'audio', 'ZIP': 'archives', 'RAR': 'archives', '7Z': 'archives', 'TAR': 'archives', 'GZ': 'archives'} module-attribute

_chunk_text(full_content, chunk_size)

Split long content into reasonably sized chunks for model input.

Source code in npcpy/data/load.py
def _chunk_text(full_content: str, chunk_size: int) -> List[str]:
    """Split long content into reasonably sized chunks for model input."""
    chunks = []
    for i in range(0, len(full_content), chunk_size):
        chunk = full_content[i:i+chunk_size].strip()
        if chunk:
            chunks.append(chunk)
    return chunks

_extract_audio_from_video(file_path, max_duration=600)

Use ffmpeg to dump the audio track from a video into a temp wav for transcription. Returns the temp path or None.

Source code in npcpy/data/load.py
def _extract_audio_from_video(file_path: str, max_duration: int = 600) -> Optional[str]:
    """
    Use ffmpeg to dump the audio track from a video into a temp wav for transcription.
    Returns the temp path or None.
    """
    try:
        temp_audio = tempfile.NamedTemporaryFile(delete=False, suffix=".wav")
        temp_audio.close()
        cmd = [
            "ffmpeg",
            "-y",
            "-i",
            file_path,
            "-vn",
            "-ac",
            "1",
            "-ar",
            "16000",
            "-t",
            str(max_duration),
            temp_audio.name,
        ]
        subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        return temp_audio.name
    except Exception:
        return None

_transcribe_audio(file_path, language=None)

Best-effort audio transcription using optional dependencies. Tries faster-whisper, then openai/whisper. Falls back to metadata only.

Source code in npcpy/data/load.py
def _transcribe_audio(file_path: str, language: Optional[str] = None) -> str:
    """
    Best-effort audio transcription using optional dependencies.
    Tries faster-whisper, then openai/whisper. Falls back to metadata only.
    """
    try:
        from npcpy.data.audio import transcribe_audio_file
        text = transcribe_audio_file(file_path, language=language)
        if text:
            return text
    except Exception:
        pass

    try:
        from faster_whisper import WhisperModel
        try:
            import torch
            device = "cuda" if torch.cuda.is_available() else "cpu"
        except Exception:
            device = "cpu"
        model = WhisperModel("small", device=device)
        segments, _ = model.transcribe(file_path, language=language, beam_size=5)
        return " ".join(seg.text.strip() for seg in segments if seg.text).strip()
    except Exception:
        pass

    try:
        import whisper
        model = whisper.load_model("small")
        result = model.transcribe(file_path, language=language)
        return result.get("text", "").strip()
    except Exception:
        pass

    return f"[Audio file at {file_path}; install faster-whisper or whisper for transcription]"

load_audio(file_path, language=None)

Load and transcribe an audio file into text.

Source code in npcpy/data/load.py
def load_audio(file_path: str, language: Optional[str] = None) -> str:
    """Load and transcribe an audio file into text."""
    transcript = _transcribe_audio(file_path, language=language)
    if transcript:
        return transcript
    return f"[Audio file at {file_path}; no transcript available]"

load_csv(file_path)

Source code in npcpy/data/load.py
def load_csv(file_path):
    df = pd.read_csv(file_path)
    return df

load_docx(file_path)

Source code in npcpy/data/load.py
def load_docx(file_path):
    if Document is None:
        raise ImportError("Please install python-docx to load .docx files.")
    doc = Document(file_path)
    full_text = "\n".join([para.text for para in doc.paragraphs])
    return full_text

load_excel(file_path)

Source code in npcpy/data/load.py
def load_excel(file_path):
    df = pd.read_excel(file_path)
    return df

load_file_contents(file_path, chunk_size=None)

Source code in npcpy/data/load.py
def load_file_contents(file_path, chunk_size=None):
    file_ext = os.path.splitext(file_path)[1].upper().lstrip('.')
    full_content = ""
    if not isinstance(chunk_size, int):
        chunk_size=8000
    try:
        if file_ext == 'PDF':
            full_content = load_pdf(file_path)
        elif file_ext == 'DOCX':
            full_content = load_docx(file_path)
        elif file_ext == 'PPTX':
            full_content = load_pptx(file_path)
        elif file_ext in ['HTML', 'HTM']:
            full_content = load_html(file_path)
        elif file_ext == 'CSV':
            df = load_csv(file_path)
            full_content = df.to_string()
        elif file_ext in ['XLS', 'XLSX']:
            df = load_excel(file_path)
            full_content = df.to_string()
        elif file_ext in ['TXT', 'MD', 'PY', 'JSX', 'TSX', 'TS', 'JS', 'JSON', 'SQL', 'NPC', 'JINX', 'LINE', 'YAML', 'DART', 'JAVA']:
            full_content = load_txt(file_path)
        elif file_ext == 'JSON':
            data = load_json(file_path)
            full_content = json.dumps(data, indent=2)
        elif file_ext in ['MP3', 'WAV', 'M4A', 'AAC', 'FLAC', 'OGG']:
            full_content = load_audio(file_path)
        elif file_ext in ['MP4', 'AVI', 'MOV', 'WMV', 'MPG', 'MPEG', 'WEBM', 'MKV']:
            full_content = load_video(file_path)
        else:
            return [f"Unsupported file format for content loading: {file_ext}"]

        if not full_content:
            return []

        return _chunk_text(full_content, chunk_size)

    except Exception as e:
        return [f"Error loading file {file_path}: {str(e)}"]

load_html(file_path)

Source code in npcpy/data/load.py
def load_html(file_path):
    if BeautifulSoup is None:
        raise ImportError("Please install beautifulsoup4 to load .html files.")
    with open(file_path, 'r', encoding='utf-8') as f:
        soup = BeautifulSoup(f, 'html.parser')
    return soup.get_text(separator='\n', strip=True)

load_image(file_path)

Source code in npcpy/data/load.py
def load_image(file_path):
    img = Image.open(file_path)
    img_array = np.array(img)
    df = pd.DataFrame(
        {
            "image_array": [img_array.tobytes()],
            "shape": [img_array.shape],
            "dtype": [img_array.dtype.str],
        }
    )
    return df

load_json(file_path)

Source code in npcpy/data/load.py
def load_json(file_path):
    with open(file_path, "r", encoding='utf-8') as f:
        data = json.load(f)
    return data

load_pdf(file_path)

Source code in npcpy/data/load.py
def load_pdf(file_path):
    reader = pypdf.PdfReader(file_path)
    full_text = ""
    for page in reader.pages:
        full_text += (page.extract_text() or "") + "\n"
    return full_text

load_pptx(file_path)

Source code in npcpy/data/load.py
def load_pptx(file_path):
    if Presentation is None:
        raise ImportError("Please install python-pptx to load .pptx files.")
    prs = Presentation(file_path)
    full_text = ""
    for slide in prs.slides:
        for shape in slide.shapes:
            if hasattr(shape, "text"):
                full_text += shape.text + "\n"
    return full_text

load_txt(file_path)

Source code in npcpy/data/load.py
def load_txt(file_path):
    with open(file_path, "r", encoding='utf-8') as f:
        text = f.read()
    return text

load_video(file_path, language=None, max_audio_seconds=600)

Summarize a video by reporting metadata and (optionally) transcribing its audio track.

Source code in npcpy/data/load.py
def load_video(file_path: str, language: Optional[str] = None, max_audio_seconds: int = 600) -> str:
    """
    Summarize a video by reporting metadata and (optionally) transcribing its audio track.
    """
    try:
        from npcpy.data.video import summarize_video_file
        return summarize_video_file(file_path, language=language, max_audio_seconds=max_audio_seconds)
    except Exception:
        pass

    meta_bits = []
    try:
        import cv2
        video = cv2.VideoCapture(file_path)
        fps = video.get(cv2.CAP_PROP_FPS)
        frame_count = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
        width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH))
        height = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))
        duration = frame_count / fps if fps else 0
        meta_bits.append(
            f"Video file: {os.path.basename(file_path)} | {width}x{height} | {fps:.2f} fps | {frame_count} frames | ~{duration:.1f}s"
        )
        video.release()
    except Exception:
        meta_bits.append(f"Video file: {os.path.basename(file_path)}")

    audio_path = _extract_audio_from_video(file_path, max_duration=max_audio_seconds)
    transcript = ""
    if audio_path:
        try:
            transcript = _transcribe_audio(audio_path, language=language)
        finally:
            try:
                os.remove(audio_path)
            except Exception:
                pass

    if transcript:
        meta_bits.append("Audio transcript:")
        meta_bits.append(transcript)
    else:
        meta_bits.append("[No transcript extracted; ensure ffmpeg and faster-whisper/whisper are installed]")

    return "\n".join(meta_bits)

audio

CHANNELS = 1 module-attribute

CHUNK = 512 module-attribute

FORMAT = pyaudio.paInt16 module-attribute

RATE = 16000 module-attribute

logger = logging.getLogger(__name__) module-attribute

audio_callback(in_data, frame_count, time_info, status)

Source code in npcpy/data/audio.py
def audio_callback(in_data, frame_count, time_info, status):
    import pyaudio
    audio_queue = queue.Queue()
    audio_queue.put(in_data)
    return (in_data, pyaudio.paContinue)

check_ffmpeg()

Source code in npcpy/data/audio.py
def check_ffmpeg():
    try:
        subprocess.run(
            ["ffmpeg", "-version"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL
        )
        return True
    except (subprocess.SubprocessError, FileNotFoundError):
        return False

convert_mp3_to_wav(mp3_file, wav_file)

Source code in npcpy/data/audio.py
def convert_mp3_to_wav(mp3_file, wav_file):
    try:

        if os.path.exists(wav_file):
            os.remove(wav_file)

        subprocess.run(
            [
                "ffmpeg",
                "-y",
                "-i",
                mp3_file,
                "-acodec",
                "pcm_s16le",
                "-ac",
                "1",
                "-ar",
                "44100",
                wav_file,
            ],
            check=True,
            capture_output=True,
            text=True,
        )
    except subprocess.CalledProcessError as e:
        print(f"Error converting MP3 to WAV: {e.stderr}")
        raise
    except Exception as e:
        print(f"Unexpected error during conversion: {e}")
        raise

create_and_queue_audio(text, state, engine='kokoro', voice=None)

Create and play TTS audio using the unified engine interface.

Parameters:
  • text –

    Text to speak

  • state –

    Dict with 'tts_is_speaking', 'tts_just_finished', 'running' keys

  • engine –

    TTS engine name (kokoro, qwen3, elevenlabs, openai, gemini, gtts)

  • voice –

    Voice ID (engine-specific)

Source code in npcpy/data/audio.py
def create_and_queue_audio(text, state, engine="kokoro", voice=None):
    """Create and play TTS audio using the unified engine interface.

    Args:
        text: Text to speak
        state: Dict with 'tts_is_speaking', 'tts_just_finished', 'running' keys
        engine: TTS engine name (kokoro, qwen3, elevenlabs, openai, gemini, gtts)
        voice: Voice ID (engine-specific)
    """
    import wave
    import uuid

    state["tts_is_speaking"] = True

    if not text.strip():
        state["tts_is_speaking"] = False
        return

    try:
        from npcpy.gen.audio_gen import text_to_speech

        audio_bytes = text_to_speech(text, engine=engine, voice=voice)

        suffix = '.mp3' if engine in ('elevenlabs', 'gtts') else '.wav'
        tmp_path = os.path.join(tempfile.gettempdir(), f"npc_tts_{uuid.uuid4()}{suffix}")
        with open(tmp_path, 'wb') as f:
            f.write(audio_bytes)

        play_path = tmp_path
        if suffix == '.mp3':
            wav_path = tmp_path.replace('.mp3', '.wav')
            convert_mp3_to_wav(tmp_path, wav_path)
            play_path = wav_path

        play_audio(play_path, state)

        for p in set([tmp_path, play_path]):
            try:
                if os.path.exists(p):
                    os.remove(p)
            except Exception:
                pass
    except Exception as e:
        logger.error(f"TTS error: {e}")
    finally:
        state["tts_is_speaking"] = False
        state["tts_just_finished"] = True

get_available_stt_engines()

Get info about available STT engines.

Source code in npcpy/data/audio.py
def get_available_stt_engines() -> dict:
    """Get info about available STT engines."""
    engines = {
        "whisper": {
            "name": "Whisper (Local)",
            "type": "local",
            "available": False,
            "description": "OpenAI Whisper running locally",
            "install": "pip install faster-whisper"
        },
        "openai": {
            "name": "OpenAI Whisper API",
            "type": "cloud",
            "available": False,
            "description": "OpenAI's cloud Whisper API",
            "requires": "OPENAI_API_KEY"
        },
        "gemini": {
            "name": "Gemini",
            "type": "cloud",
            "available": False,
            "description": "Google Gemini transcription",
            "requires": "GOOGLE_API_KEY or GEMINI_API_KEY"
        },
        "elevenlabs": {
            "name": "ElevenLabs Scribe",
            "type": "cloud",
            "available": False,
            "description": "ElevenLabs speech-to-text",
            "requires": "ELEVENLABS_API_KEY"
        },
        "groq": {
            "name": "Groq Whisper",
            "type": "cloud",
            "available": False,
            "description": "Ultra-fast Whisper via Groq",
            "requires": "GROQ_API_KEY"
        }
    }

    try:
        from faster_whisper import WhisperModel
        engines["whisper"]["available"] = True
    except ImportError:
        try:
            import whisper
            engines["whisper"]["available"] = True
        except ImportError:
            pass

    if os.environ.get('OPENAI_API_KEY'):
        engines["openai"]["available"] = True

    if os.environ.get('GOOGLE_API_KEY') or os.environ.get('GEMINI_API_KEY'):
        engines["gemini"]["available"] = True

    if os.environ.get('ELEVENLABS_API_KEY'):
        engines["elevenlabs"]["available"] = True

    if os.environ.get('GROQ_API_KEY'):
        engines["groq"]["available"] = True

    return engines

play_audio(filename, state)

Play a WAV file via pyaudio with state awareness.

Source code in npcpy/data/audio.py
def play_audio(filename, state):
    """Play a WAV file via pyaudio with state awareness."""
    import pyaudio
    import wave

    PLAY_CHUNK = 4096

    wf = wave.open(filename, "rb")
    p = pyaudio.PyAudio()

    stream = p.open(
        format=p.get_format_from_width(wf.getsampwidth()),
        channels=wf.getnchannels(),
        rate=wf.getframerate(),
        output=True,
    )

    data = wf.readframes(PLAY_CHUNK)
    while data and state.get("running", True):
        stream.write(data)
        data = wf.readframes(PLAY_CHUNK)

    stream.stop_stream()
    stream.close()
    p.terminate()

process_text_for_tts(text)

Clean text for TTS consumption.

Source code in npcpy/data/audio.py
def process_text_for_tts(text):
    """Clean text for TTS consumption."""
    text = re.sub(r"[*<>{}()\[\]&%#@^~`]", "", text)
    text = text.strip()
    text = re.sub(r"(\w)\.(\w)\.", r"\1 \2 ", text)
    text = re.sub(r"([.!?])(\w)", r"\1 \2", text)
    return text

run_transcription(audio_np)

Source code in npcpy/data/audio.py
def run_transcription(audio_np):
    try:
        temp_file = os.path.join(
            tempfile.gettempdir(), f"temp_recording_{int(time.time())}.wav"
        )
        with wave.open(temp_file, "wb") as wf:
            wf.setnchannels(CHANNELS)
            wf.setsampwidth(2)
            wf.setframerate(RATE)
            wf.writeframes((audio_np * 32768).astype(np.int16).tobytes())
        whisper_model = WhisperModel("large-v3", device="cuda" if torch.cuda.is_available() else "cpu")
        segments, info = whisper_model.transcribe(temp_file, language="en", beam_size=5)
        transcription = " ".join([segment.text for segment in segments])

        try:
            if os.path.exists(temp_file):
                os.remove(temp_file)
        except Exception:
            if temp_file not in cleanup_files:
                cleanup_files.append(temp_file)

        return transcription.strip()

    except Exception as e:
        print(f"Transcription error: {str(e)}")
        return None

speech_to_text(audio_data, engine='whisper', language=None, **kwargs)

Unified STT interface.

Parameters:
  • audio_data (bytes) –

    Audio bytes (WAV, MP3, etc.)

  • engine (str, default: 'whisper' ) –

    STT engine (whisper, openai, gemini, elevenlabs, groq)

  • language (str, default: None ) –

    Language hint

  • **kwargs –

    Engine-specific options

Returns:
  • dict –

    Dict with at least 'text' key

Source code in npcpy/data/audio.py
def speech_to_text(
    audio_data: bytes,
    engine: str = "whisper",
    language: str = None,
    **kwargs
) -> dict:
    """
    Unified STT interface.

    Args:
        audio_data: Audio bytes (WAV, MP3, etc.)
        engine: STT engine (whisper, openai, gemini, elevenlabs, groq)
        language: Language hint
        **kwargs: Engine-specific options

    Returns:
        Dict with at least 'text' key
    """
    engine = engine.lower()

    if engine == "whisper" or engine == "faster-whisper":
        try:
            return stt_whisper(audio_data, language=language, **kwargs)
        except ImportError:
            import whisper
            with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
                f.write(audio_data)
                temp_path = f.name
            try:
                model = whisper.load_model(kwargs.get("model_size", "base"))
                result = model.transcribe(temp_path, language=language)
                return {"text": result["text"].strip(), "language": result.get("language", "en")}
            finally:
                os.unlink(temp_path)

    elif engine == "openai":
        return stt_openai(audio_data, language=language, **kwargs)

    elif engine == "gemini":
        return stt_gemini(audio_data, language=language, **kwargs)

    elif engine == "elevenlabs":
        return stt_elevenlabs(audio_data, language=language, **kwargs)

    elif engine == "groq":
        return stt_groq(audio_data, language=language, **kwargs)

    else:
        raise ValueError(f"Unknown STT engine: {engine}")

stt_elevenlabs(audio_data, api_key=None, model_id='scribe_v1', language=None)

Transcribe audio using ElevenLabs Scribe API.

Parameters:
  • audio_data (bytes) –

    Audio bytes

  • api_key (str, default: None ) –

    ElevenLabs API key

  • model_id (str, default: 'scribe_v1' ) –

    Model (scribe_v1)

  • language (str, default: None ) –

    Language code (ISO 639-1)

Returns:
  • dict –

    Dict with 'text', 'language', 'words'

Source code in npcpy/data/audio.py
def stt_elevenlabs(
    audio_data: bytes,
    api_key: str = None,
    model_id: str = "scribe_v1",
    language: str = None
) -> dict:
    """
    Transcribe audio using ElevenLabs Scribe API.

    Args:
        audio_data: Audio bytes
        api_key: ElevenLabs API key
        model_id: Model (scribe_v1)
        language: Language code (ISO 639-1)

    Returns:
        Dict with 'text', 'language', 'words'
    """
    import requests

    api_key = api_key or os.environ.get('ELEVENLABS_API_KEY')
    if not api_key:
        raise ValueError("ELEVENLABS_API_KEY not set")

    url = "https://api.elevenlabs.io/v1/speech-to-text"
    headers = {"xi-api-key": api_key}

    files = {"file": ("audio.wav", audio_data, "audio/wav")}
    data = {"model_id": model_id}
    if language:
        data["language_code"] = language

    response = requests.post(url, headers=headers, files=files, data=data)
    response.raise_for_status()

    result = response.json()
    return {
        "text": result.get("text", "").strip(),
        "language": result.get("language_code"),
        "words": result.get("words", [])
    }

stt_gemini(audio_data, api_key=None, model='gemini-1.5-flash', language=None, mime_type='audio/wav')

Transcribe audio using Gemini API.

Parameters:
  • audio_data (bytes) –

    Audio bytes

  • api_key (str, default: None ) –

    Google/Gemini API key

  • model (str, default: 'gemini-1.5-flash' ) –

    Gemini model

  • language (str, default: None ) –

    Language hint

  • mime_type (str, default: 'audio/wav' ) –

    Audio MIME type

Returns:
  • dict –

    Dict with 'text'

Source code in npcpy/data/audio.py
def stt_gemini(
    audio_data: bytes,
    api_key: str = None,
    model: str = "gemini-1.5-flash",
    language: str = None,
    mime_type: str = "audio/wav"
) -> dict:
    """
    Transcribe audio using Gemini API.

    Args:
        audio_data: Audio bytes
        api_key: Google/Gemini API key
        model: Gemini model
        language: Language hint
        mime_type: Audio MIME type

    Returns:
        Dict with 'text'
    """
    import google.generativeai as genai

    api_key = api_key or os.environ.get('GOOGLE_API_KEY') or os.environ.get('GEMINI_API_KEY')
    if not api_key:
        raise ValueError("GOOGLE_API_KEY or GEMINI_API_KEY not set")

    genai.configure(api_key=api_key)
    model_obj = genai.GenerativeModel(model)

    prompt = "Transcribe this audio exactly. Output only the transcription, nothing else."
    if language:
        prompt = f"Transcribe this audio in {language}. Output only the transcription, nothing else."

    response = model_obj.generate_content([
        prompt,
        {"mime_type": mime_type, "data": audio_data}
    ])

    return {"text": response.text.strip()}

stt_groq(audio_data, api_key=None, model='whisper-large-v3', language=None)

Transcribe audio using Groq's Whisper API (very fast).

Parameters:
  • audio_data (bytes) –

    Audio bytes

  • api_key (str, default: None ) –

    Groq API key

  • model (str, default: 'whisper-large-v3' ) –

    whisper-large-v3 or whisper-large-v3-turbo

  • language (str, default: None ) –

    Language code

Returns:
  • dict –

    Dict with 'text'

Source code in npcpy/data/audio.py
def stt_groq(
    audio_data: bytes,
    api_key: str = None,
    model: str = "whisper-large-v3",
    language: str = None
) -> dict:
    """
    Transcribe audio using Groq's Whisper API (very fast).

    Args:
        audio_data: Audio bytes
        api_key: Groq API key
        model: whisper-large-v3 or whisper-large-v3-turbo
        language: Language code

    Returns:
        Dict with 'text'
    """
    import requests

    api_key = api_key or os.environ.get('GROQ_API_KEY')
    if not api_key:
        raise ValueError("GROQ_API_KEY not set")

    url = "https://api.groq.com/openai/v1/audio/transcriptions"
    headers = {"Authorization": f"Bearer {api_key}"}

    files = {"file": ("audio.wav", audio_data, "audio/wav")}
    data = {"model": model}
    if language:
        data["language"] = language

    response = requests.post(url, headers=headers, files=files, data=data)
    response.raise_for_status()

    return {"text": response.json().get("text", "").strip()}

stt_openai(audio_data, api_key=None, model='whisper-1', language=None, response_format='verbose_json', filename='audio.wav')

Transcribe audio using OpenAI Whisper API.

Parameters:
  • audio_data (bytes) –

    Audio bytes

  • api_key (str, default: None ) –

    OpenAI API key

  • model (str, default: 'whisper-1' ) –

    Model name (whisper-1)

  • language (str, default: None ) –

    Optional language hint

  • response_format (str, default: 'verbose_json' ) –

    json, text, srt, verbose_json, vtt

  • filename (str, default: 'audio.wav' ) –

    Filename hint for format detection

Returns:
  • dict –

    Dict with 'text', 'language', 'segments' (if verbose_json)

Source code in npcpy/data/audio.py
def stt_openai(
    audio_data: bytes,
    api_key: str = None,
    model: str = "whisper-1",
    language: str = None,
    response_format: str = "verbose_json",
    filename: str = "audio.wav"
) -> dict:
    """
    Transcribe audio using OpenAI Whisper API.

    Args:
        audio_data: Audio bytes
        api_key: OpenAI API key
        model: Model name (whisper-1)
        language: Optional language hint
        response_format: json, text, srt, verbose_json, vtt
        filename: Filename hint for format detection

    Returns:
        Dict with 'text', 'language', 'segments' (if verbose_json)
    """
    import requests

    api_key = api_key or os.environ.get('OPENAI_API_KEY')
    if not api_key:
        raise ValueError("OPENAI_API_KEY not set")

    url = "https://api.openai.com/v1/audio/transcriptions"
    headers = {"Authorization": f"Bearer {api_key}"}

    files = {"file": (filename, audio_data)}
    data = {"model": model, "response_format": response_format}
    if language:
        data["language"] = language

    response = requests.post(url, headers=headers, files=files, data=data)
    response.raise_for_status()

    if response_format == "verbose_json":
        result = response.json()
        return {
            "text": result.get("text", "").strip(),
            "language": result.get("language", "en"),
            "duration": result.get("duration"),
            "segments": result.get("segments", [])
        }
    elif response_format == "json":
        return {"text": response.json().get("text", "").strip()}
    else:
        return {"text": response.text.strip()}

stt_whisper(audio_data, model_size='base', language=None, device='auto')

Transcribe audio using local Whisper (faster-whisper).

Parameters:
  • audio_data (bytes) –

    Audio bytes (WAV, MP3, etc.)

  • model_size (str, default: 'base' ) –

    Model size (tiny, base, small, medium, large-v3)

  • language (str, default: None ) –

    Language code or None for auto-detect

  • device (str, default: 'auto' ) –

    'cpu', 'cuda', or 'auto'

Returns:
  • dict –

    Dict with 'text', 'language', 'segments'

Source code in npcpy/data/audio.py
def stt_whisper(
    audio_data: bytes,
    model_size: str = "base",
    language: str = None,
    device: str = "auto"
) -> dict:
    """
    Transcribe audio using local Whisper (faster-whisper).

    Args:
        audio_data: Audio bytes (WAV, MP3, etc.)
        model_size: Model size (tiny, base, small, medium, large-v3)
        language: Language code or None for auto-detect
        device: 'cpu', 'cuda', or 'auto'

    Returns:
        Dict with 'text', 'language', 'segments'
    """
    from faster_whisper import WhisperModel

    if device == "auto":
        try:
            import torch
            device = "cuda" if torch.cuda.is_available() else "cpu"
        except ImportError:
            device = "cpu"

    compute_type = "float16" if device == "cuda" else "int8"
    model = WhisperModel(model_size, device=device, compute_type=compute_type)

    with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
        f.write(audio_data)
        temp_path = f.name

    try:
        segments, info = model.transcribe(
            temp_path,
            language=language,
            beam_size=5,
            vad_filter=True
        )

        segment_list = []
        text_parts = []
        for segment in segments:
            segment_list.append({
                "start": segment.start,
                "end": segment.end,
                "text": segment.text
            })
            text_parts.append(segment.text)

        return {
            "text": " ".join(text_parts).strip(),
            "language": info.language,
            "language_probability": info.language_probability,
            "segments": segment_list
        }
    finally:
        os.unlink(temp_path)

transcribe_audio_file(file_path, language=None)

File-based transcription helper that prefers the local faster-whisper/whisper setup used elsewhere in this module.

Source code in npcpy/data/audio.py
def transcribe_audio_file(file_path: str, language=None) -> str:
    """
    File-based transcription helper that prefers the local faster-whisper/whisper
    setup used elsewhere in this module.
    """
    try:
        from faster_whisper import WhisperModel
        try:
            import torch
            device = "cuda" if torch.cuda.is_available() else "cpu"
        except Exception:
            device = "cpu"
        model = WhisperModel("small", device=device)
        segments, _ = model.transcribe(file_path, language=language, beam_size=5)
        text = " ".join(seg.text.strip() for seg in segments if seg.text).strip()
        if text:
            return text
    except Exception:
        pass

    try:
        import whisper
        model = whisper.load_model("small")
        result = model.transcribe(file_path, language=language)
        text = result.get("text", "").strip()
        if text:
            return text
    except Exception:
        pass

    return ""

transcribe_recording(audio_data)

Source code in npcpy/data/audio.py
def transcribe_recording(audio_data):
    if not audio_data:
        return None

    audio_np = (
        np.frombuffer(b"".join(audio_data), dtype=np.int16).astype(np.float32) / 32768.0
    )
    return run_transcription(audio_np)

image

_windows_snip_to_file(file_path)

Helper function to trigger Windows snipping and save to file.

Source code in npcpy/data/image.py
def _windows_snip_to_file(file_path: str) -> bool:
    """Helper function to trigger Windows snipping and save to file."""
    try:

        import win32clipboard
        from PIL import ImageGrab
        from ctypes import windll


        windll.user32.keybd_event(0x5B, 0, 0, 0)  
        windll.user32.keybd_event(0x10, 0, 0, 0)  
        windll.user32.keybd_event(0x53, 0, 0, 0)  
        windll.user32.keybd_event(0x53, 0, 0x0002, 0)  
        windll.user32.keybd_event(0x10, 0, 0x0002, 0)  
        windll.user32.keybd_event(0x5B, 0, 0x0002, 0)  


        print("Please select an area to capture...")
        time.sleep(1)  


        max_wait = 30  
        start_time = time.time()

        while time.time() - start_time < max_wait:
            try:
                image = ImageGrab.grabclipboard()
                if image:
                    image.save(file_path, "PNG")
                    return True
            except Exception:
                pass
            time.sleep(0.5)

        return False

    except ImportError:
        print("Required packages not found. Please install: pip install pywin32 Pillow")
        return False

capture_screenshot(full=False)

Function Description

This function captures a screenshot of the current screen and saves it to a file.

Args: npc: The NPC object representing the current NPC. full: Boolean to determine if full screen capture is needed. Default to true. path: Optional path to save the screenshot. Must not use placeholders. Relative paths preferred if the user specifies they want a specific path, otherwise default to None. Returns: A dictionary containing the filename, file path, and model kwargs.

Source code in npcpy/data/image.py
def capture_screenshot( full=False) -> Dict[str, str]:
    """
    Function Description:
        This function captures a screenshot of the current screen and saves it to a file.
    Args:
        npc: The NPC object representing the current NPC.
        full: Boolean to determine if full screen capture is needed. Default to true.
        path: Optional path to save the screenshot. Must not use placeholders. Relative paths preferred if the user specifies they want a specific path, otherwise default to None.
    Returns:
        A dictionary containing the filename, file path, and model kwargs.
    """


    directory = os.path.expanduser("~/.npcsh/screenshots")
    timestamp = time.strftime("%Y%m%d_%H%M%S")
    filename = f"screenshot_{timestamp}.png"

    file_path = os.path.join(directory, filename)
    os.makedirs(directory, exist_ok=True)



    system = platform.system()

    model_kwargs = {}

    if full:

        if system.lower() == "darwin":

            subprocess.run(["screencapture", file_path], capture_output=True)

        elif system == "Linux":
            _took = False
            for _cmd, _args in [
                ("grim", [file_path]),
                ("scrot", [file_path]),
                ("import", ["-window", "root", file_path]),
                ("gnome-screenshot", ["-f", file_path]),
            ]:
                if subprocess.run(["which", _cmd], capture_output=True).returncode == 0:
                    subprocess.run([_cmd] + _args, capture_output=True, timeout=10)
                    if os.path.exists(file_path):
                        _took = True
                        break
            if not _took:
                print("No supported screenshot tool found. Install scrot, grim, or imagemagick.")

        elif system == "Windows":

            try:
                import win32gui
                import win32ui
                import win32con
                from PIL import Image


                width = win32api.GetSystemMetrics(win32con.SM_CXVIRTUALSCREEN)
                height = win32api.GetSystemMetrics(win32con.SM_CYVIRTUALSCREEN)


                hdesktop = win32gui.GetDesktopWindow()
                desktop_dc = win32gui.GetWindowDC(hdesktop)
                img_dc = win32ui.CreateDCFromHandle(desktop_dc)
                mem_dc = img_dc.CreateCompatibleDC()


                screenshot = win32ui.CreateBitmap()
                screenshot.CreateCompatibleBitmap(img_dc, width, height)
                mem_dc.SelectObject(screenshot)
                mem_dc.BitBlt((0, 0), (width, height), img_dc, (0, 0), win32con.SRCCOPY)


                screenshot.SaveBitmapFile(mem_dc, file_path)


                mem_dc.DeleteDC()
                win32gui.DeleteObject(screenshot.GetHandle())

            except ImportError:
                print(
                    "Required packages not found. Please install: pip install pywin32"
                )
                return None
        else:
            print(f"Unsupported operating system: {system}")
            return None
    else:
        if system == "Darwin":
            subprocess.run(["screencapture", "-i", file_path])
        elif system == "Linux":
            if (
                subprocess.run(
                    ["which", "gnome-screenshot"], capture_output=True
                ).returncode
                == 0
            ):
                subprocess.Popen(["gnome-screenshot", "-a", "-f", file_path])
                while not os.path.exists(file_path):
                    time.sleep(0.5)
            elif (
                subprocess.run(["which", "scrot"], capture_output=True).returncode == 0
            ):
                subprocess.Popen(["scrot", "-s", file_path])
                while not os.path.exists(file_path):
                    time.sleep(0.5)
            else:
                print(
                    "No supported screenshot jinx found. Please install gnome-screenshot or scrot."
                )
                return None
        elif system == "Windows":
            success = _windows_snip_to_file(file_path)
            if not success:
                print("Screenshot capture failed or timed out.")
                return None
        else:
            print(f"Unsupported operating system: {system}")
            return None


    if os.path.exists(file_path):
        print(f"Screenshot saved to: {file_path}")
        return {
            "filename": filename,
            "file_path": file_path,
            "model_kwargs": model_kwargs,
        }
    else:
        print("Screenshot capture failed or was cancelled.")
        return None

compress_image(image_bytes, max_size=(800, 600))

Source code in npcpy/data/image.py
def compress_image(image_bytes, max_size=(800, 600)):

    buffer = io.BytesIO(image_bytes)
    img = Image.open(buffer)


    img.load()


    if img.mode == "RGBA":
        background = Image.new("RGB", img.size, (255, 255, 255))
        background.paste(img, mask=img.split()[3])
        img = background


    if img.size[0] > max_size[0] or img.size[1] > max_size[1]:
        img.thumbnail(max_size)


    out_buffer = io.BytesIO()
    img.save(out_buffer, format="JPEG", quality=95, optimize=False)
    return out_buffer.getvalue()

text

load_all_files(directory, extensions=None, depth=1)

Function Description

This function loads all text files in a directory and its subdirectories.

Args: directory: The directory to search. Keyword Args: extensions: A list of file extensions to include. depth: The depth of subdirectories to search. Returns: A dictionary with file paths as keys and file contents as values.

Source code in npcpy/data/text.py
def load_all_files(
    directory: str, extensions: List[str] = None, depth: int = 1
) -> Dict[str, str]:
    """
    Function Description:
        This function loads all text files in a directory and its subdirectories.
    Args:
        directory: The directory to search.
    Keyword Args:
        extensions: A list of file extensions to include.
        depth: The depth of subdirectories to search.
    Returns:
        A dictionary with file paths as keys and file contents as values.
    """
    text_data = {}
    if depth < 1:
        return text_data  

    if extensions is None:

        extensions = [
            ".txt",
            ".md",
            ".py",
            ".java",
            ".c",
            ".cpp",
            ".html",
            ".css",
            ".js",
            ".ts",
            ".tsx",
            ".npc",

        ]

    try:

        entries = os.listdir(directory)
    except Exception as e:
        print(f"Could not list directory {directory}: {e}")
        return text_data

    for entry in entries:
        path = os.path.join(directory, entry)
        if os.path.isfile(path):
            if any(path.endswith(ext) for ext in extensions):
                try:
                    with open(path, "r", encoding="utf-8", errors="ignore") as file:
                        text_data[path] = file.read()
                except Exception as e:
                    print(f"Could not read file {path}: {e}")
        elif os.path.isdir(path):

            subdir_data = load_all_files(path, extensions, depth=depth - 1)
            text_data.update(subdir_data)

    return text_data
Function Description

This function retrieves lines from documents that are relevant to the query.

Args: query: The query string. text_data: A dictionary with file paths as keys and file contents as values. embedding_model: The sentence embedding model. Keyword Args: text_data_embedded: A dictionary with file paths as keys and embedded file contents as values. similarity_threshold: The similarity threshold for considering a line relevant. Returns: A list of relevant snippets.

Source code in npcpy/data/text.py
def rag_search(
    query: str,
    text_data: Union[Dict[str, str], str],
    embedding_model: Any = None,
    text_data_embedded: Optional[Dict[str, np.ndarray]] = None,
    similarity_threshold: float = 0.3,
    device="cpu",
) -> List[str]:
    """
    Function Description:
        This function retrieves lines from documents that are relevant to the query.
    Args:
        query: The query string.
        text_data: A dictionary with file paths as keys and file contents as values.
        embedding_model: The sentence embedding model.
    Keyword Args:
        text_data_embedded: A dictionary with file paths as keys and embedded file contents as values.
        similarity_threshold: The similarity threshold for considering a line relevant.
    Returns:
        A list of relevant snippets.

    """
    if embedding_model is None:
        try:
            embedding_model = SentenceTransformer("all-MiniLM-L6-v2")
        except Exception:
            raise Exception(
                "Please install the sentence-transformers library to use this function or provide an embedding transformer model."
            )
    results = []


    query_embedding = embedding_model.encode(
        query, convert_to_tensor=True, show_progress_bar=False
    )
    if isinstance(text_data, str):

        lines = text_data.split(".")
        if not lines:
            return results

        if text_data_embedded is None:
            line_embeddings = embedding_model.encode(lines, convert_to_tensor=True)
        else:
            line_embeddings = text_data_embedded

        cosine_scores = util.cos_sim(query_embedding, line_embeddings)[0].cpu().numpy()


        relevant_line_indices = np.where(cosine_scores >= similarity_threshold)[0]




        for idx in relevant_line_indices:
            idx = int(idx)

            start_idx = max(0, idx - 10)
            end_idx = min(len(lines), idx + 11)  
            snippet = ". ".join(lines[start_idx:end_idx])
            results.append(snippet)

    elif isinstance(text_data, dict):
        for filename, content in text_data.items():

            lines = content.split("\n")
            if not lines:
                continue

            if text_data_embedded is None:
                line_embeddings = embedding_model.encode(lines, convert_to_tensor=True)
            else:
                line_embeddings = text_data_embedded[filename]

            cosine_scores = (
                util.cos_sim(query_embedding, line_embeddings)[0].cpu().numpy()
            )




            relevant_line_indices = np.where(cosine_scores >= similarity_threshold)[0]



            for idx in relevant_line_indices:
                idx = int(idx)  

                start_idx = max(0, idx - 10)
                end_idx = min(
                    len(lines), idx + 11
                )  
                snippet = "\n".join(lines[start_idx:end_idx])
                results.append((filename, snippet))

    return results

video

process_video(file_path, table_name)

Source code in npcpy/data/video.py
def process_video(file_path, table_name):

    import cv2
    import base64

    embeddings = []
    texts = []
    try:
        video = cv2.VideoCapture(file_path)
        fps = video.get(cv2.CAP_PROP_FPS)
        frame_count = int(video.get(cv2.CAP_PROP_FRAME_COUNT))

        for i in range(frame_count):
            ret, frame = video.read()
            if not ret:
                break


            n = 10  

        return embeddings, texts

    except Exception as e:
        print(f"Error processing video: {e}")
        return [], []  

summarize_video_file(file_path, language=None, max_audio_seconds=600)

Summarize a video using lightweight metadata plus optional audio transcript. Prefers the audio transcription helper in npcpy.data.audio when available.

Source code in npcpy/data/video.py
def summarize_video_file(file_path: str, language: str = None, max_audio_seconds: int = 600) -> str:
    """
    Summarize a video using lightweight metadata plus optional audio transcript.
    Prefers the audio transcription helper in npcpy.data.audio when available.
    """
    meta_bits = []
    try:
        import cv2

        video = cv2.VideoCapture(file_path)
        fps = video.get(cv2.CAP_PROP_FPS)
        frame_count = int(video.get(cv2.CAP_PROP_FRAME_COUNT))
        width = int(video.get(cv2.CAP_PROP_FRAME_WIDTH))
        height = int(video.get(cv2.CAP_PROP_FRAME_HEIGHT))
        duration = frame_count / fps if fps else 0
        meta_bits.append(
            f"Video file: {os.path.basename(file_path)} | {width}x{height} | {fps:.2f} fps | {frame_count} frames | ~{duration:.1f}s"
        )
        video.release()
    except Exception:
        meta_bits.append(f"Video file: {os.path.basename(file_path)}")

    audio_path = None
    try:
        temp_audio = tempfile.NamedTemporaryFile(delete=False, suffix=".wav")
        temp_audio.close()
        cmd = [
            "ffmpeg",
            "-y",
            "-i",
            file_path,
            "-vn",
            "-ac",
            "1",
            "-ar",
            "16000",
            "-t",
            str(max_audio_seconds),
            temp_audio.name,
        ]
        subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        audio_path = temp_audio.name
    except Exception:
        audio_path = None

    transcript = ""
    if audio_path:
        try:
            try:
                from npcpy.data.audio import transcribe_audio_file
                transcript = transcribe_audio_file(audio_path, language=language)
            except Exception:
                transcript = ""
        finally:
            try:
                os.remove(audio_path)
            except Exception:
                pass

    if transcript:
        meta_bits.append("Audio transcript:")
        meta_bits.append(transcript)
    else:
        meta_bits.append("[No transcript extracted; ensure ffmpeg and a transcription backend are installed]")

    return "\n".join(meta_bits)

web

search_brave(query, num_results=5, api_key=None)

Search using Brave Search API.

Source code in npcpy/data/web.py
def search_brave(query: str, num_results: int = 5, api_key: str = None):
    """Search using Brave Search API."""
    if api_key is None:
        api_key = os.environ.get('BRAVE_API_KEY')
    if not api_key:
        print("Brave API key not set")
        return []

    try:
        response = requests.get(
            'https://api.search.brave.com/res/v1/web/search',
            params={'q': query, 'count': num_results},
            headers={'X-Subscription-Token': api_key, 'Accept': 'application/json'},
            timeout=10
        )
        if response.status_code == 200:
            data = response.json()
            results = []
            for r in data.get('web', {}).get('results', [])[:num_results]:
                results.append({
                    'title': r.get('title', ''),
                    'link': r.get('url', ''),
                    'content': r.get('description', '')
                })
            return results
    except Exception as e:
        print(f"Brave search failed: {e}")
    return []

search_exa(query, api_key=None, top_k=5, search_type='auto', **kwargs)

Search using Exa - the fastest and most accurate web search API for AI.

Source code in npcpy/data/web.py
def search_exa(query:str,
               api_key:str = None,
               top_k = 5,
               search_type = "auto",
               **kwargs):
    """Search using Exa - the fastest and most accurate web search API for AI."""
    from exa_py import Exa
    if api_key is None:
        api_key = os.environ.get('EXA_API_KEY')
    exa = Exa(api_key)
    exa.headers["x-exa-integration"] = "npcpy"

    results = exa.search_and_contents(
        query,
        type=search_type,
        text=True
    )
    return results.results[0:top_k]

search_perplexity(query, api_key=None, model='sonar', max_tokens=400, temperature=0.2, top_p=0.9)

Source code in npcpy/data/web.py
def search_perplexity(
    query: str,
    api_key: str = None,
    model: str = "sonar",
    max_tokens: int = 400,
    temperature: float = 0.2,
    top_p: float = 0.9,
):
    if api_key is None:
        api_key = os.environ.get("PERPLEXITY_API_KEY")
        if api_key is None:
            raise ValueError("PERPLEXITY_API_KEY not set. Set it in your environment or ~/.npcshrc.")


    url = "https://api.perplexity.ai/chat/completions"
    payload = {
        "model": "sonar",
        "messages": [
            {"role": "system", "content": "Be precise and concise."},
            {"role": "user", "content": query},
        ],
        "max_tokens": max_tokens,
        "temperature": temperature,
        "top_p": top_p,
        "return_images": False,
        "return_related_questions": False,
        "top_k": 0,
        "stream": False,
        "presence_penalty": 0,
        "frequency_penalty": 1,
        "response_format": None,
    }


    headers = {"Authorization": f"Bearer {api_key}", 
               "Content-Type": "application/json"}

    response = requests.post(url,
                             json=payload,
                             headers=headers)

    if response.status_code != 200:
        raise Exception(f"Perplexity API error {response.status_code}: {response.text[:200]}")

    data = response.json()
    return [data["choices"][0]["message"]["content"], data.get("citations", [])]

search_searxng(query, num_results=5, instance_url=None)

Search using SearXNG public instances.

Source code in npcpy/data/web.py
def search_searxng(query: str, num_results: int = 5, instance_url: str = None):
    """Search using SearXNG public instances."""
    instances = [instance_url] if instance_url else [
        os.environ.get('SEARXNG_URL'),
        'https://search.sapti.me',
        'https://searx.work',
        'https://search.ononoki.org',
        'https://searx.tiekoetter.com',
        'https://search.bus-hit.me',
        'https://priv.au',
    ]
    instances = [i for i in instances if i]

    for instance in instances:
        try:
            response = requests.get(
                f"{instance}/search",
                params={'q': query, 'format': 'json', 'categories': 'general'},
                headers={'User-Agent': 'Mozilla/5.0 (X11; Linux x86_64; rv:121.0) Gecko/20100101 Firefox/121.0'},
                timeout=10
            )
            if response.status_code == 200:
                data = response.json()
                results = []
                for r in data.get('results', [])[:num_results]:
                    results.append({
                        'title': r.get('title', ''),
                        'link': r.get('url', ''),
                        'content': r.get('content', '')
                    })
                if results:
                    return results
        except Exception as e:
            print(f"SearXNG {instance} failed: {e}")
            continue
    return []

search_startpage(query, num_results=5)

Search using Startpage (scraping).

Source code in npcpy/data/web.py
def search_startpage(query: str, num_results: int = 5):
    """Search using Startpage (scraping)."""
    try:
        response = requests.post(
            'https://www.startpage.com/sp/search',
            data={'query': query, 'cat': 'web'},
            headers={
                'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
                'Accept': 'text/html',
            },
            timeout=10
        )
        if response.status_code == 200:
            soup = BeautifulSoup(response.text, 'html.parser')
            results = []
            for item in soup.select('.result')[:num_results]:
                title_el = item.select_one('h2') or item.select_one('a')
                link_el = item.select_one('a[href^="http"]')
                desc_el = item.select_one('p') or item.select_one('.description')
                if link_el:
                    results.append({
                        'title': title_el.get_text(strip=True) if title_el else '',
                        'link': link_el.get('href', ''),
                        'content': desc_el.get_text(strip=True) if desc_el else ''
                    })
            return results
    except Exception as e:
        print(f"Startpage search failed: {e}")
    return []

search_web(query, num_results=5, provider=None, api_key=None, perplexity_kwargs=None)

Function Description

This function searches the web for information based on a query.

Args: query: The search query. Keyword Args: num_results: The number of search results to retrieve. provider: The search engine provider to use ('perplexity' or 'duckduckgo'). Returns: A list of dictionaries with 'title', 'link', and 'content' keys.

Source code in npcpy/data/web.py
def search_web(
    query: str,
    num_results: int = 5,
    provider: str=None,
    api_key=None,
    perplexity_kwargs: Optional[Dict[str, Any]] = None,
) -> List:
    """
    Function Description:
        This function searches the web for information based on a query.
    Args:
        query: The search query.
    Keyword Args:
        num_results: The number of search results to retrieve.
        provider: The search engine provider to use ('perplexity' or 'duckduckgo').
    Returns:
        A list of dictionaries with 'title', 'link', and 'content' keys.
    """
    if perplexity_kwargs is None:
        perplexity_kwargs = {}
    results = []
    if provider is None:
        provider = 'startpage'

    if provider == "perplexity":
        search_result = search_perplexity(query, api_key=api_key, **perplexity_kwargs)

        return search_result

    if provider == "startpage":
        results = search_startpage(query, num_results=num_results)
        if results:
            return results
        print("Startpage failed, falling back to searxng")
        provider = 'searxng'

    if provider == "searxng":
        results = search_searxng(query, num_results=num_results)
        if results:
            return results
        print("SearXNG failed, falling back to duckduckgo")
        provider = 'duckduckgo'

    if provider == "duckduckgo":
        headers = {
            "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:124.0) Gecko/20100101 Firefox/124.0"
        }
        try:
            ddgs = DDGS(headers=headers)
            search_results = ddgs.text(query, max_results=num_results)
            results = [
                {"title": r["title"], "link": r["href"], "content": r["body"]}
                for r in search_results
            ]
        except Exception as e:
            print("DuckDuckGo search failed: ", e)
            results = []
    elif provider == 'exa':
        return search_exa(query, api_key=api_key, top_k=num_results)

    elif provider == 'startpage':
        results = search_startpage(query, num_results=num_results)

    elif provider == 'brave':
        results = search_brave(query, num_results=num_results, api_key=api_key)

    elif provider == 'google':
        if _google_search is None:
            print("googlesearch-python not installed, falling back to duckduckgo")
            return search_web(query, num_results=num_results, provider='duckduckgo')
        urls = list(_google_search(query, num_results=num_results))



        for url in urls:
            try:

                headers = {
                    "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
                }
                response = requests.get(url, headers=headers, timeout=5)
                response.raise_for_status()


                soup = BeautifulSoup(response.text, "html.parser")


                title = soup.title.string if soup.title else url


                content = " ".join([p.get_text() for p in soup.find_all("p")])
                content = " ".join(content.split())  

                results.append(
                    {
                        "title": title,
                        "link": url,
                        "content": (
                            content[:500] + "..." if len(content) > 500 else content
                        ),
                    }
                )

            except Exception as e:
                print(f"Error fetching {url}: {str(e)}")
                continue



    content_str = "\n".join(
        [r["content"] + "\n Citation: " + r["link"] + "\n\n\n" for r in results]
    )
    link_str = "\n".join([r["link"] + "\n" for r in results])
    return [content_str, link_str]