...
This commit is contained in:
0
herolib/clients/__init__.py
Normal file
0
herolib/clients/__init__.py
Normal file
0
herolib/clients/assemblyai/__init__.py
Normal file
0
herolib/clients/assemblyai/__init__.py
Normal file
69
herolib/clients/assemblyai/client.py
Normal file
69
herolib/clients/assemblyai/client.py
Normal file
@@ -0,0 +1,69 @@
|
||||
import os
|
||||
|
||||
from pydub import AudioSegment
|
||||
import assemblyai as aai
|
||||
|
||||
|
||||
class Client:
|
||||
def __init__(self):
|
||||
api_key = os.getenv("ASSEMBLYAI")
|
||||
|
||||
if not api_key:
|
||||
raise EnvironmentError(
|
||||
"Please set the ASSEMBLYAI environment variable with your AssemblyAI API key."
|
||||
)
|
||||
|
||||
self.api_key = api_key
|
||||
aai.settings.api_key = self.api_key
|
||||
self.transcriber = aai.Transcriber()
|
||||
|
||||
def convert_to_ogg_mono(self, input_path: str, output_path: str):
|
||||
"""Converts an audio file from .mp4 to .ogg (mono)."""
|
||||
audio = AudioSegment.from_file(input_path, format="mp4")
|
||||
# Convert to mono if needed by uncommenting the line below
|
||||
# audio = audio.set_channels(1)
|
||||
audio.export(output_path, format="ogg")
|
||||
print(f"Converted to .ogg in {output_path}")
|
||||
|
||||
def transcribe_audio(self, audio_path: str, output_path: str):
|
||||
"""Transcribes the audio file and saves the transcription to a Markdown file."""
|
||||
config = aai.TranscriptionConfig(
|
||||
speaker_labels=True,
|
||||
)
|
||||
|
||||
transcript = self.transcriber.transcribe(audio_path, config)
|
||||
|
||||
with open(output_path, "w", encoding="utf-8") as f:
|
||||
for utterance in transcript.utterances:
|
||||
f.write(
|
||||
f"** Speaker {utterance.speaker}:\n{utterance.text}\n-------------\n"
|
||||
)
|
||||
|
||||
print(f"Transcription saved to {output_path}")
|
||||
|
||||
def transcribe_audio_file(self, input_path: str, output_transcription_path: str):
|
||||
"""Handles the entire process from conversion to transcription and cleanup."""
|
||||
converted_audio_path = input_path.replace(".mp4", ".ogg")
|
||||
|
||||
# Convert .mp4 to .ogg
|
||||
self.convert_to_ogg_mono(input_path, converted_audio_path)
|
||||
|
||||
# Perform the transcription
|
||||
self.transcribe_audio(converted_audio_path, output_transcription_path)
|
||||
|
||||
# Optionally, clean up the converted file
|
||||
os.remove(converted_audio_path)
|
||||
print(f"Removed temporary file {converted_audio_path}")
|
||||
|
||||
|
||||
# Example usage:
|
||||
if __name__ == "__main__":
|
||||
# Retrieve API key from environment variable
|
||||
|
||||
# Define the paths for the input audio and output transcription
|
||||
input_audio_path = "/tmp/475353425.mp4"
|
||||
output_transcription_path = "/tmp/transcribe_475353425.md"
|
||||
|
||||
# Perform the transcription process
|
||||
client = Client()
|
||||
client.transcribe_audio_file(input_audio_path, output_transcription_path)
|
19
herolib/clients/readme.md
Normal file
19
herolib/clients/readme.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# Vimeo Client
|
||||
|
||||
need following functionality
|
||||
|
||||
- upload video
|
||||
- download
|
||||
- list video's
|
||||
|
||||
## some info
|
||||
|
||||
- https://developer.vimeo.com/api/reference
|
||||
|
||||
## remarks to use make sure you have the secrets
|
||||
|
||||
```bash
|
||||
hero git clone -u git@git.threefold.info:despiegk/hero_secrets.git
|
||||
source git.threefold.info/projectmycelium/hero_server/myenv.sh
|
||||
```
|
||||
|
0
herolib/clients/stellar/__init__.py
Normal file
0
herolib/clients/stellar/__init__.py
Normal file
241
herolib/clients/stellar/horizon.py
Normal file
241
herolib/clients/stellar/horizon.py
Normal file
@@ -0,0 +1,241 @@
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from typing import List, Optional
|
||||
from stellar_sdk import Keypair, Server, StrKey
|
||||
import json
|
||||
import redis
|
||||
from stellar.model import StellarAsset, StellarAccount
|
||||
import os
|
||||
import csv
|
||||
import toml
|
||||
from herotools.texttools import description_fix
|
||||
|
||||
|
||||
|
||||
class HorizonServer:
|
||||
def __init__(self, instance: str = "default", network: str = "main", tomlfile: str = "", owner: str = ""):
|
||||
"""
|
||||
Load a Stellar account's information using the Horizon server.
|
||||
The Horizon server is an API that allows interaction with the Stellar network. It provides endpoints to submit transactions, check account balances, and perform other operations on the Stellar ledger.
|
||||
All gets cached in redis
|
||||
"""
|
||||
self.redis_client = redis.Redis(host='localhost', port=6379, db=0) # Adjust as needed
|
||||
self.instance = instance
|
||||
if network not in ['main', 'testnet']:
|
||||
raise ValueError("Invalid network value. Must be 'main' or 'testnet'.")
|
||||
self.network = network
|
||||
testnet = self.network == 'testnet'
|
||||
self.server = Server("https://horizon-testnet.stellar.org" if testnet else "https://horizon.stellar.org")
|
||||
self.tomlfile = os.path.expanduser(tomlfile)
|
||||
self.owner = owner
|
||||
if self.tomlfile:
|
||||
self.toml_load()
|
||||
|
||||
def account_exists(self, pubkey: str) -> bool:
|
||||
"""
|
||||
Check if an account exists in the Redis cache based on the public key.
|
||||
"""
|
||||
redis_key = f"stellar:{self.instance}:accounts:{pubkey}"
|
||||
return self.redis_client.exists(redis_key) != None
|
||||
|
||||
def account_get(self, key: str, reload: bool = False, name: str = "", description: str = "", cat: str = "") -> StellarAccount:
|
||||
"""
|
||||
Load a Stellar account's information.
|
||||
|
||||
Args:
|
||||
key (str): The private or public key of the Stellar account.
|
||||
reset (bool, optional): Whether to force a refresh of the cached data. Defaults to False.
|
||||
name (str, optional): Name for the account. Defaults to "".
|
||||
description (str, optional): Description for the account. Defaults to "".
|
||||
owner (str, optional): Owner of the account. Defaults to "".
|
||||
cat (str, optional): Category of the account. Defaults to "".
|
||||
|
||||
Returns:
|
||||
StellarAccount: A struct containing the account's information.
|
||||
"""
|
||||
|
||||
if key == "" and name:
|
||||
for redis_key in self.redis_client.scan_iter(f"stellar:{self.instance}:accounts:*"):
|
||||
data = self.redis_client.get(redis_key)
|
||||
if data:
|
||||
data = json.loads(str(data))
|
||||
if data.get('name') == name and data.get('priv_key', data.get('public_key')):
|
||||
key = data.get('priv_key', data.get('public_key'))
|
||||
break
|
||||
|
||||
if key == "":
|
||||
raise ValueError("No key provided")
|
||||
|
||||
# Determine if the key is a public or private key
|
||||
if StrKey.is_valid_ed25519_public_key(key):
|
||||
public_key = key
|
||||
priv_key = ""
|
||||
elif StrKey.is_valid_ed25519_secret_seed(key):
|
||||
priv_key = key
|
||||
keypair = Keypair.from_secret(priv_key)
|
||||
public_key = keypair.public_key
|
||||
else:
|
||||
raise ValueError("Invalid Stellar key provided")
|
||||
|
||||
redis_key = f"stellar:{self.instance}:accounts:{public_key}"
|
||||
|
||||
data = self.redis_client.get(redis_key)
|
||||
changed = False
|
||||
if data:
|
||||
try:
|
||||
data = json.loads(str(data))
|
||||
except Exception as e:
|
||||
print(data)
|
||||
raise e
|
||||
data['assets'] = [StellarAsset(**asset) for asset in data['assets']]
|
||||
account = StellarAccount(**data)
|
||||
if description!="" and description!=account.description:
|
||||
account.description = description
|
||||
changed = True
|
||||
if name!="" and name!=account.name:
|
||||
account.name = name
|
||||
changed = True
|
||||
if self.owner!="" and self.owner!=account.owner:
|
||||
account.owner = self.owner
|
||||
changed = True
|
||||
if cat!="" and cat!=account.cat:
|
||||
account.cat = cat
|
||||
changed = True
|
||||
else:
|
||||
account = StellarAccount(public_key=public_key, description=description, name=name, priv_key=priv_key, owner=self.owner, cat=cat)
|
||||
changed = True
|
||||
|
||||
|
||||
if reload or account.assets == []:
|
||||
changed = True
|
||||
if reload:
|
||||
account.assets = []
|
||||
account_data = self.server.accounts().account_id(public_key).call()
|
||||
account.assets.clear() # Clear existing assets to avoid duplication
|
||||
for balance in account_data['balances']:
|
||||
asset_type = balance['asset_type']
|
||||
if asset_type == 'native':
|
||||
account.assets.append(StellarAsset(type="XLM", balance=balance['balance']))
|
||||
else:
|
||||
if 'asset_code' in balance:
|
||||
account.assets.append(StellarAsset(
|
||||
type=balance['asset_code'],
|
||||
issuer=balance['asset_issuer'],
|
||||
balance=balance['balance']
|
||||
))
|
||||
changed = True
|
||||
|
||||
# Cache the result in Redis for 1 hour if there were changes
|
||||
if changed:
|
||||
self.account_save(account)
|
||||
|
||||
return account
|
||||
|
||||
def comment_add(self, pubkey: str, comment: str, ignore_non_exist: bool = False):
|
||||
"""
|
||||
Add a comment to a Stellar account based on the public key.
|
||||
|
||||
Args:
|
||||
pubkey (str): The public key of the Stellar account.
|
||||
comment (str): The comment to add to the account.
|
||||
"""
|
||||
comment = description_fix(comment)
|
||||
if not self.account_exists(pubkey):
|
||||
if ignore_non_exist:
|
||||
return
|
||||
raise ValueError("Account does not exist in the cache")
|
||||
account = self.account_get(pubkey)
|
||||
account.comments.append(comment)
|
||||
self.account_save(account)
|
||||
|
||||
def account_save(self, account: StellarAccount):
|
||||
"""
|
||||
Save a Stellar account's information to the Redis cache.
|
||||
|
||||
Args:
|
||||
account (StellarAccount): The account to save.
|
||||
"""
|
||||
redis_key = f"stellar:{self.instance}:accounts:{account.public_key}"
|
||||
self.redis_client.setex(redis_key, 600, json.dumps(asdict(account)))
|
||||
|
||||
def reload_cache(self):
|
||||
"""
|
||||
Walk over all known accounts and reload their information.
|
||||
"""
|
||||
for redis_key in self.redis_client.scan_iter(f"stellar:{self.instance}:accounts:*"):
|
||||
data = self.redis_client.get(redis_key) or ""
|
||||
if data:
|
||||
data = json.loads(str(data))
|
||||
public_key = data.get('public_key')
|
||||
if public_key:
|
||||
self.account_get(public_key, reload=True)
|
||||
|
||||
|
||||
#format is PUBKEY,DESCRIPTION in text format
|
||||
def load_accounts_csv(self, file_path:str):
|
||||
file_path=os.path.expanduser(file_path)
|
||||
if not os.path.exists(file_path):
|
||||
return Exception(f"Error: File '{file_path}' does not exist.")
|
||||
try:
|
||||
with open(file_path, 'r', newline='') as file:
|
||||
reader = csv.reader(file, delimiter=',')
|
||||
for row in reader:
|
||||
if row and len(row) >= 2: # Check if row is not empty and has at least 2 elements
|
||||
pubkey = row[0].strip()
|
||||
comment = ','.join(row[1:]).strip()
|
||||
if self.account_exists(pubkey):
|
||||
self.comment_add(pubkey, comment)
|
||||
except IOError as e:
|
||||
return Exception(f"Error reading file: {e}")
|
||||
except csv.Error as e:
|
||||
return Exception(f"Error parsing CSV: {e}")
|
||||
except Exception as e:
|
||||
return Exception(f"Error: {e}")
|
||||
|
||||
def accounts_get(self) -> List[StellarAccount]:
|
||||
"""
|
||||
Retrieve a list of all known Stellar accounts from the Redis cache.
|
||||
|
||||
Returns:
|
||||
List[StellarAccount]: A list of StellarAccount objects.
|
||||
"""
|
||||
accounts = []
|
||||
for redis_key in self.redis_client.scan_iter(f"stellar:{self.instance}:accounts:*"):
|
||||
pubkey = str(redis_key.split(':')[-1])
|
||||
accounts.append(self.account_get(key=pubkey))
|
||||
return accounts
|
||||
|
||||
def toml_save(self):
|
||||
"""
|
||||
Save the list of all known Stellar accounts to a TOML file.
|
||||
|
||||
Args:
|
||||
file_path (str): The path where the list needs to be saved.
|
||||
"""
|
||||
if self.tomlfile == "":
|
||||
raise ValueError("No TOML file path provided")
|
||||
accounts = self.accounts_get()
|
||||
accounts_dict = {account.public_key: asdict(account) for account in accounts}
|
||||
with open(self.tomlfile, 'w') as file:
|
||||
toml.dump( accounts_dict, file)
|
||||
|
||||
def toml_load(self):
|
||||
"""
|
||||
Load the list of Stellar accounts from a TOML file and save them to the Redis cache.
|
||||
|
||||
Args:
|
||||
file_path (str): The path of the TOML file to load.
|
||||
"""
|
||||
if not os.path.exists(self.tomlfile):
|
||||
return
|
||||
#raise FileNotFoundError(f"Error: File '{self.tomlfile}' does not exist.")
|
||||
with open(self.tomlfile, 'r') as file:
|
||||
accounts_dict = toml.load(file)
|
||||
for pubkey, account_data in accounts_dict.items():
|
||||
account_data['assets'] = [StellarAsset(**asset) for asset in account_data['assets']]
|
||||
account = StellarAccount(**account_data)
|
||||
self.account_save(account)
|
||||
|
||||
|
||||
|
||||
def new(instance: str = "default",owner: str = "", network: str = "main", tomlfile: str = "") -> HorizonServer:
|
||||
return HorizonServer(instance=instance, network=network, tomlfile=tomlfile,owner=owner)
|
70
herolib/clients/stellar/model.py
Normal file
70
herolib/clients/stellar/model.py
Normal file
@@ -0,0 +1,70 @@
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from typing import List, Optional
|
||||
from stellar_sdk import Keypair, Server, StrKey
|
||||
import json
|
||||
import redis
|
||||
|
||||
@dataclass
|
||||
class StellarAsset:
|
||||
type: str
|
||||
balance: float
|
||||
issuer: str = ""
|
||||
|
||||
def format_balance(self):
|
||||
balance_float = float(self.balance)
|
||||
formatted_balance = f"{balance_float:,.2f}"
|
||||
if '.' in formatted_balance:
|
||||
formatted_balance = formatted_balance.rstrip('0').rstrip('.')
|
||||
return formatted_balance
|
||||
|
||||
def md(self):
|
||||
formatted_balance = self.format_balance()
|
||||
return f"- **{self.type}**: {formatted_balance}"
|
||||
|
||||
@dataclass
|
||||
class StellarAccount:
|
||||
owner: str
|
||||
priv_key: str = ""
|
||||
public_key: str = ""
|
||||
assets: List[StellarAsset] = field(default_factory=list)
|
||||
name: str = ""
|
||||
description: str = ""
|
||||
comments: List[str] = field(default_factory=list)
|
||||
cat: str = ""
|
||||
question: str = ""
|
||||
|
||||
def md(self):
|
||||
result = [
|
||||
f"# Stellar Account: {self.name or 'Unnamed'}","",
|
||||
f"**Public Key**: {self.public_key}",
|
||||
f"**Cat**: {self.cat}",
|
||||
f"**Description**: {self.description[:60]}..." if self.description else "**Description**: None",
|
||||
f"**Question**: {self.question}" if self.question else "**Question**: None",
|
||||
"",
|
||||
"## Assets:",""
|
||||
]
|
||||
|
||||
for asset in self.assets:
|
||||
result.append(asset.md())
|
||||
|
||||
if len(self.assets) == 0:
|
||||
result.append("- No assets")
|
||||
|
||||
result.append("")
|
||||
|
||||
if self.comments:
|
||||
result.append("## Comments:")
|
||||
for comment in self.comments:
|
||||
if '\n' in comment:
|
||||
multiline_comment = "\n ".join(comment.split('\n'))
|
||||
result.append(f"- {multiline_comment}")
|
||||
else:
|
||||
result.append(f"- {comment}")
|
||||
|
||||
return "\n".join(result)
|
||||
|
||||
def balance_str(self) -> str:
|
||||
out=[]
|
||||
for asset in self.assets:
|
||||
out.append(f"{asset.type}:{float(asset.balance):,.0f}")
|
||||
return " ".join(out)
|
78
herolib/clients/stellar/model_accounts.v
Normal file
78
herolib/clients/stellar/model_accounts.v
Normal file
@@ -0,0 +1,78 @@
|
||||
module stellar
|
||||
import freeflowuniverse.crystallib.core.texttools
|
||||
|
||||
pub struct DigitalAssets {
|
||||
pub mut:
|
||||
|
||||
|
||||
}
|
||||
|
||||
pub struct Owner {
|
||||
pub mut:
|
||||
name string
|
||||
accounts []Account
|
||||
}
|
||||
|
||||
@[params]
|
||||
pub struct AccountGetArgs{
|
||||
pub mut:
|
||||
name string
|
||||
bctype BlockChainType
|
||||
}
|
||||
|
||||
pub fn (self DigitalAssets) account_get(args_ AccountGetArgs) !&Account {
|
||||
|
||||
mut accounts := []&Account
|
||||
mut args:=args_
|
||||
|
||||
args.name = texttools.name_fix(args.name)
|
||||
|
||||
for account in self.accounts {
|
||||
if account.name == args.name && account.bctype == args.bctype {
|
||||
accounts<<&account
|
||||
}
|
||||
}
|
||||
|
||||
if accounts.len == 0 {
|
||||
return error('No account found with the given name:${args.name} and blockchain type: ${args.bctype}')
|
||||
} else if count > 1 {
|
||||
return error('Multiple accounts found with the given name:${args.name} and blockchain type: ${args.bctype}')
|
||||
}
|
||||
|
||||
return accounts[0]
|
||||
}
|
||||
|
||||
pub struct Account {
|
||||
pub mut:
|
||||
name string
|
||||
secret string
|
||||
pubkey string
|
||||
description string
|
||||
cat string
|
||||
owner string
|
||||
assets []Asset
|
||||
bctype BlockChainType
|
||||
}
|
||||
|
||||
pub struct Asset {
|
||||
pub mut:
|
||||
amount int
|
||||
assettype AssetType
|
||||
}
|
||||
|
||||
pub fn (self Asset) name() string {
|
||||
return self.assettype.name
|
||||
}
|
||||
|
||||
pub struct AssetType {
|
||||
pub mut:
|
||||
name string
|
||||
issuer string
|
||||
bctype BlockChainType
|
||||
}
|
||||
|
||||
pub enum BlockChainType{
|
||||
stellar_pub
|
||||
stellar_test
|
||||
|
||||
}
|
46
herolib/clients/stellar/testnet.py
Normal file
46
herolib/clients/stellar/testnet.py
Normal file
@@ -0,0 +1,46 @@
|
||||
from typing import Tuple
|
||||
from stellar_sdk import Server, Keypair, TransactionBuilder, Network, Asset, Signer, TransactionEnvelope
|
||||
import redis
|
||||
import requests
|
||||
import json
|
||||
import time
|
||||
|
||||
def create_account_on_testnet() -> Tuple[str, str]:
|
||||
|
||||
def fund(public_key: str) -> float:
|
||||
# Request funds from the Stellar testnet friendbot
|
||||
response = requests.get(f"https://friendbot.stellar.org?addr={public_key}")
|
||||
if response.status_code != 200:
|
||||
raise Exception("Failed to fund new account with friendbot")
|
||||
time.sleep(1)
|
||||
return balance(public_key)
|
||||
|
||||
def create_account() -> Tuple[str, str]:
|
||||
# Initialize Redis client
|
||||
redis_client = redis.StrictRedis(host='localhost', port=6379, db=0)
|
||||
|
||||
# Generate keypair
|
||||
keypair = Keypair.random()
|
||||
public_key = keypair.public_key
|
||||
secret_key = keypair.secret
|
||||
account_data = {
|
||||
"public_key": public_key,
|
||||
"secret_key": secret_key
|
||||
}
|
||||
redis_client.set("stellartest:testaccount", json.dumps(account_data))
|
||||
time.sleep(1)
|
||||
return public_key, secret_key
|
||||
|
||||
# Check if the account already exists in Redis
|
||||
if redis_client.exists("stellartest:testaccount"):
|
||||
account_data = json.loads(redis_client.get("stellartest:testaccount"))
|
||||
public_key = account_data["public_key"]
|
||||
secret_key = account_data["secret_key"]
|
||||
r = balance(public_key)
|
||||
if r < 100:
|
||||
fund(public_key)
|
||||
r = balance(public_key)
|
||||
return public_key, secret_key
|
||||
else:
|
||||
create_account()
|
||||
return create_account_on_testnet()
|
0
herolib/clients/telegram/__init__.py
Normal file
0
herolib/clients/telegram/__init__.py
Normal file
102
herolib/clients/telegram/bot.py
Normal file
102
herolib/clients/telegram/bot.py
Normal file
@@ -0,0 +1,102 @@
|
||||
import json
|
||||
import redis
|
||||
import telebot
|
||||
import os
|
||||
import logging
|
||||
from termcolor import colored
|
||||
from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
from telebot.formatting import escape_markdown
|
||||
|
||||
from bot_audio import audio_add
|
||||
from bot_text import text_add
|
||||
from ai.ask import ai_assistent,AIAssistant
|
||||
|
||||
class MyBot:
|
||||
def __init__(self,ai_reset:bool=False):
|
||||
# Initialize logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(message)s')
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
# Initialize Redis connection
|
||||
self.redis_client = redis.Redis(host='localhost', port=6379, db=0)
|
||||
|
||||
# Initialize Telegram bot
|
||||
self.telebotkey = os.getenv("TELEBOT")
|
||||
if self.telebotkey:
|
||||
self.logger.info(colored("TELEBOT key set", "green"))
|
||||
self.bot = telebot.TeleBot(self.telebotkey)
|
||||
else:
|
||||
raise Exception("can't find TELEBOT in ENV")
|
||||
|
||||
# Set up message handlers
|
||||
self.setup_handlers()
|
||||
audio_add(self)
|
||||
text_add(self,reset=ai_reset)
|
||||
|
||||
def setup_handlers(self):
|
||||
@self.bot.message_handler(commands=['help'])
|
||||
def send_welcome(message):
|
||||
self.bot.reply_to(message, """\
|
||||
Hi there, I am your hero.
|
||||
Just speak to me or do /start or /help
|
||||
""")
|
||||
|
||||
@self.bot.message_handler(commands=['start'])
|
||||
def start_command(message):
|
||||
chat_id = message.chat.id
|
||||
|
||||
keyboard = InlineKeyboardMarkup()
|
||||
subscribe_button = InlineKeyboardButton("Subscribe to Updates", callback_data='subscribe')
|
||||
unsubscribe_button = InlineKeyboardButton("Unsubscribe from Updates", callback_data='unsubscribe')
|
||||
keyboard.row(subscribe_button, unsubscribe_button)
|
||||
|
||||
self.bot.reply_to(message, "Please choose an option:", reply_markup=keyboard)
|
||||
|
||||
@self.bot.callback_query_handler(func=lambda call: True)
|
||||
def callback_query(call):
|
||||
chat_id = call.message.chat.id
|
||||
|
||||
if call.data == 'subscribe':
|
||||
self.redis_client.hset('subscribed_chats', chat_id, '1')
|
||||
self.bot.answer_callback_query(call.id, "You have subscribed to updates.")
|
||||
print(f"User subscribed to updates: {chat_id}")
|
||||
elif call.data == 'unsubscribe':
|
||||
self.redis_client.hdel('subscribed_chats', chat_id)
|
||||
self.bot.answer_callback_query(call.id, "You have unsubscribed from updates.")
|
||||
print(f"User unsubscribed from updates: {chat_id}")
|
||||
|
||||
def send_message_to_subscribers(self, message):
|
||||
subscribed_chats = self.redis_client.hgetall('subscribed_chats')
|
||||
for chat_id in subscribed_chats:
|
||||
try:
|
||||
self.bot.send_message(chat_id.decode('utf-8'), message)
|
||||
except Exception as e:
|
||||
print(f"Failed to send message to chat {chat_id}: {str(e)}")
|
||||
|
||||
def send_error_to_telegram(self,chat_id, error_message):
|
||||
# Format the error message for Telegram
|
||||
telegram_message = f"🚨 Error Occurred 🚨\n\n"
|
||||
telegram_message += f"app: {escape_markdown(error_message['app'])}\n"
|
||||
telegram_message += f"Function: {escape_markdown(error_message['function'])}\n"
|
||||
telegram_message += f"msg: {escape_markdown(error_message['msg'])}\n"
|
||||
telegram_message += f"Exception Type: {escape_markdown(error_message['exception_type'])}\n"
|
||||
telegram_message += f"Exception Message: ```\n{escape_markdown(error_message['exception_message'])}\n```\n"
|
||||
if 'traceback' in error_message:
|
||||
telegram_message += f"Traceback:\n```\n{escape_markdown(error_message['traceback'])}\n```"
|
||||
# Send the error message to the subscribed chat
|
||||
self.bot.send_message(chat_id, telegram_message, parse_mode='Markdown')
|
||||
|
||||
|
||||
def start(self):
|
||||
print("Bot started")
|
||||
# Start the bot
|
||||
self.bot.polling()
|
||||
|
||||
|
||||
def bot_new() -> MyBot:
|
||||
return MyBot()
|
||||
|
||||
# Usage
|
||||
if __name__ == "__main__":
|
||||
my_bot = bot_new()
|
||||
my_bot.start()
|
72
herolib/clients/telegram/bot_audio.py
Normal file
72
herolib/clients/telegram/bot_audio.py
Normal file
@@ -0,0 +1,72 @@
|
||||
import os
|
||||
from pydub import AudioSegment
|
||||
import whisper
|
||||
|
||||
def audio_add(self):
|
||||
|
||||
self.model = whisper.load_model("base")
|
||||
|
||||
@self.bot.message_handler(content_types=['audio', 'voice']) #, 'document'
|
||||
def handle_audio(message):
|
||||
try:
|
||||
chat_id = message.chat.id
|
||||
file_info = None
|
||||
audio_path = None
|
||||
|
||||
if message.content_type == 'audio':
|
||||
file_info = self.bot.get_file(message.audio.file_id)
|
||||
audio_path = f"/tmp/audio/{message.audio.file_id}.mp3"
|
||||
elif message.content_type == 'voice':
|
||||
file_info = self.bot.get_file(message.voice.file_id)
|
||||
audio_path = f"/tmp/audio/{message.voice.file_id}.ogg"
|
||||
|
||||
if file_info:
|
||||
downloaded_file = self.bot.download_file(file_info.file_path)
|
||||
|
||||
# Ensure the directory exists
|
||||
os.makedirs(os.path.dirname(audio_path), exist_ok=True)
|
||||
|
||||
# Save the audio file
|
||||
with open(audio_path, 'wb') as new_file:
|
||||
new_file.write(downloaded_file)
|
||||
|
||||
#bot.send_message(chat_id, f"Audio received and saved successfully to {audio_path}.")
|
||||
print(f"Audio received and saved to {audio_path}")
|
||||
|
||||
|
||||
# Convert to WAV format if necessary
|
||||
wav_path = audio_path.replace('.mp3', '.wav').replace('.ogg', '.wav')
|
||||
if audio_path.endswith('.mp3') or audio_path.endswith('.ogg'):
|
||||
audio = AudioSegment.from_file(audio_path)
|
||||
audio.export(wav_path, format='wav')
|
||||
else:
|
||||
wav_path = audio_path
|
||||
|
||||
# Transcribe audio using Whisper
|
||||
result = self.model.transcribe(wav_path)
|
||||
transcription = result["text"]
|
||||
|
||||
self.bot.send_message(chat_id, transcription, parse_mode='Markdown')
|
||||
print(f"Audio received and saved to {audio_path}")
|
||||
print(f"Transcription: {transcription}")
|
||||
|
||||
text2 = self.text_process(self,transcription)
|
||||
|
||||
print(f"Processed text {chat_id}: {text2}")
|
||||
|
||||
if len(text2)>0:
|
||||
self.bot.send_message(chat_id, text2)
|
||||
|
||||
|
||||
|
||||
except Exception as e:
|
||||
error_message = {
|
||||
'app': 'Telegram Bot',
|
||||
'function': 'handle_audio',
|
||||
'msg': 'Failed to process audio file',
|
||||
'exception_type': type(e).__name__,
|
||||
'exception_message': str(e)
|
||||
}
|
||||
self.send_error_to_telegram(chat_id, error_message)
|
||||
print(f"Error processing audio file: {e}")
|
||||
|
51
herolib/clients/telegram/bot_text.py
Normal file
51
herolib/clients/telegram/bot_text.py
Normal file
@@ -0,0 +1,51 @@
|
||||
import os
|
||||
from ai.ask import ai_assistent
|
||||
|
||||
def text_add(self,reset:bool=False):
|
||||
|
||||
self.ai_assistent = ai_assistent(reset=reset)
|
||||
self.text_process = text_process
|
||||
|
||||
@self.bot.message_handler(content_types=['text'])
|
||||
def handle_text(message):
|
||||
try:
|
||||
chat_id = message.chat.id
|
||||
|
||||
text = message.text
|
||||
|
||||
# Here you can add your logic to process the text
|
||||
# For now, let's just echo the message back
|
||||
# response = f"You said: {text}"
|
||||
|
||||
print(f"Received text from {chat_id}: {text}")
|
||||
|
||||
text2 = self.text_process(self,text)
|
||||
|
||||
print(f"Processed text {chat_id}: {text2}")
|
||||
|
||||
if len(text2)>0:
|
||||
self.bot.send_message(chat_id, text2)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
error_message = {
|
||||
'app': 'Telegram Bot',
|
||||
'function': 'handle_text',
|
||||
'msg': 'Failed to process text',
|
||||
'exception_type': type(e).__name__,
|
||||
'exception_message': str(e)
|
||||
}
|
||||
self.send_error_to_telegram(chat_id, error_message)
|
||||
print(f"Error processing text file: {e}")
|
||||
|
||||
|
||||
def text_process(self, txt) -> str:
|
||||
if "translate" not in txt.lower():
|
||||
txt+='''\n\n
|
||||
only output the heroscript, no comments
|
||||
'''
|
||||
response = self.ai_assistent.ask(
|
||||
category='timemgmt',
|
||||
name='schedule',
|
||||
question=txt)
|
||||
return response
|
36
herolib/clients/telegram/errorqueue.py
Normal file
36
herolib/clients/telegram/errorqueue.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import json
|
||||
import redis
|
||||
import telebot
|
||||
import threading
|
||||
from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton
|
||||
import time
|
||||
from telebot.formatting import escape_markdown
|
||||
import os
|
||||
from telegram.bot import send_error_to_telegram
|
||||
|
||||
# Initialize Redis connection
|
||||
redis_client = redis.Redis(host='localhost', port=6379, db=0)
|
||||
|
||||
#get errors from redis and send them to bot if subscription done
|
||||
def process_error_queue():
|
||||
while True:
|
||||
# Pop an error message from the Redis queue
|
||||
error_json = redis_client.lpop('error_queue')
|
||||
|
||||
if error_json:
|
||||
# Deserialize the error message from JSON
|
||||
error_message = json.loads(error_json)
|
||||
|
||||
# Get all subscribed chat IDs from Redis
|
||||
subscribed_chats = redis_client.hgetall('subscribed_chats')
|
||||
|
||||
# Send the error message to all subscribed chats
|
||||
for chat_id in subscribed_chats.keys():
|
||||
send_error_to_telegram(int(chat_id), error_message)
|
||||
else:
|
||||
# If the queue is empty, wait for a short interval before checking again
|
||||
time.sleep(1)
|
||||
|
||||
# Start processing the error queue
|
||||
process_error_queue_thread = threading.Thread(target=process_error_queue)
|
||||
process_error_queue_thread.start()
|
0
herolib/clients/vimeo/__init__.py
Normal file
0
herolib/clients/vimeo/__init__.py
Normal file
142
herolib/clients/vimeo/client.py
Normal file
142
herolib/clients/vimeo/client.py
Normal file
@@ -0,0 +1,142 @@
|
||||
import os
|
||||
from typing import List, Optional
|
||||
|
||||
import requests
|
||||
import vimeo
|
||||
from model_video import VideoInfo, video_model_load, videos_model_load
|
||||
|
||||
|
||||
class VimeoClient:
|
||||
def __init__(self):
|
||||
# Retrieve necessary credentials from environment variables
|
||||
self.client_id = os.getenv("VIMEO_CLIENT_ID")
|
||||
self.client_secret = os.getenv("VIMEO_SECRET")
|
||||
self.access_token = os.getenv("VIMEO_ACCESSTOKEN_ID")
|
||||
self.user_id = os.getenv("VIMEO_USER_ID")
|
||||
|
||||
# Check if all environment variables are present
|
||||
if not all([self.client_id, self.client_secret, self.access_token, self.user_id]):
|
||||
raise EnvironmentError(
|
||||
"Please set the VIMEO_CLIENT_ID, VIMEO_SECRET,VIMEO_USER_ID and VIMEO_ACCESSTOKEN_ID environment variables."
|
||||
)
|
||||
|
||||
# Initialize the Vimeo client
|
||||
self.client = vimeo.VimeoClient(token=self.access_token, key=self.client_id, secret=self.client_secret)
|
||||
|
||||
def upload(self, file: str, video_title: str, description: str) -> str:
|
||||
video_uri = self.client.upload(file, data={"name": video_title, "description": description})
|
||||
return video_uri
|
||||
|
||||
def download(self, video_id: str, output_file: str = "myvid.mp4"):
|
||||
info = self.get_video_info(video_id)
|
||||
|
||||
size, link = 0, ""
|
||||
for item in info.download:
|
||||
if item["size"] > size:
|
||||
size = item["size"]
|
||||
link = item["link"]
|
||||
|
||||
if link == "":
|
||||
raise Exception("download link not provided for video")
|
||||
|
||||
video_response = requests.get(link, stream=True)
|
||||
downloaded_mb = 0
|
||||
with open(output_file, "wb") as video_file:
|
||||
for chunk in video_response.iter_content(chunk_size=1024):
|
||||
if chunk:
|
||||
downloaded_mb += len(chunk) / 1024
|
||||
print(f"{downloaded_mb}MB Downloaded...")
|
||||
video_file.write(chunk)
|
||||
|
||||
print(f"Video downloaded successfully to {output_file}!")
|
||||
|
||||
def get_video_info(self, video_id: str) -> VideoInfo:
|
||||
"""
|
||||
Get information about a video by URI.
|
||||
:param uri: URI of the Vimeo video.
|
||||
:return: Video information as a dictionary, or None if an error occurs.
|
||||
"""
|
||||
# , fields: List[str]
|
||||
response = self.client.get(f"/videos/{video_id}")
|
||||
if response.status_code == 200:
|
||||
myvideo = video_model_load(response.content)
|
||||
else:
|
||||
raise Exception(f"Failed to get video details. Status code: {response.status_code}, Error: {response.text}")
|
||||
return myvideo
|
||||
|
||||
def get_videos(self, folder: Optional[int] = None, folders: Optional[List[int]] = None) -> List[VideoInfo]:
|
||||
"""
|
||||
Get information about videos from specified folder(s) or all videos if no folder is specified.
|
||||
:param folder: ID of a single folder to fetch videos from.
|
||||
:param folders: List of folder IDs to fetch videos from.
|
||||
:return: List of VideoInfo objects.
|
||||
"""
|
||||
if self.user_id == 0:
|
||||
raise Exception("Can't find user ID, it's not set in env variables")
|
||||
|
||||
all_videos = []
|
||||
|
||||
if folder is not None:
|
||||
folders = [folder]
|
||||
elif folders is None:
|
||||
# If no folder or folders specified, get all videos
|
||||
response = self.client.get("/me/videos")
|
||||
if response.status_code == 200:
|
||||
return videos_model_load(response.content)
|
||||
else:
|
||||
raise Exception(f"Failed to get videos. Status code: {response.status_code}, Error: {response.text}")
|
||||
for folder_id in folders:
|
||||
response = self.client.get(f"/users/{self.user_id}/projects/{folder_id}/videos")
|
||||
if response.status_code == 200:
|
||||
videos = videos_model_load(response.content)
|
||||
all_videos.extend(videos)
|
||||
else:
|
||||
print(f"Failed to get videos for folder {folder_id}. Status code: {response.status_code}, Error: {response.text}")
|
||||
|
||||
return all_videos
|
||||
|
||||
# def get_videos(self,folder:int,folders:List[int]) -> List[VideoInfo]:
|
||||
# """
|
||||
# Get information about a video by URI.
|
||||
# :param uri: URI of the Vimeo video.
|
||||
# :return: Video information as a dictionary, or None if an error occurs.
|
||||
# """
|
||||
# if folder>0:
|
||||
# if self.user_id == 0:
|
||||
# return Exception("can't find userid, its not set in env variables")
|
||||
# # print(f"folderid:{folder}")
|
||||
# response = self.client.get(f"/users/{self.user_id}/projects/{folder}/videos")
|
||||
# # api_url = f"https://api.vimeo.com/users/{self.user_id}/projects/13139570/videos"
|
||||
# # print(api_url)
|
||||
# # access_token = "e65daca3b0dbc18c2fadc5cafcf81004"
|
||||
# # headers = {
|
||||
# # "Authorization": f"Bearer {access_token}"
|
||||
# # }
|
||||
# # Make the GET request to the Vimeo API
|
||||
# #response = requests.get(api_url, headers=headers)
|
||||
# else:
|
||||
# response = self.client.get(f"/me/videos/")
|
||||
|
||||
# if response.status_code == 200:
|
||||
# myvideos = videos_model_load(response.content)
|
||||
# else:
|
||||
# raise Exception(f"Failed to get video details. Status code: {response.status_code}, Error: {response.text}")
|
||||
# return myvideos
|
||||
|
||||
|
||||
def new() -> VimeoClient:
|
||||
return VimeoClient()
|
||||
|
||||
|
||||
# Example usage:
|
||||
if __name__ == "__main__":
|
||||
cl = new()
|
||||
v = cl.get_videos(folders=[10700101, 13139570, 12926235, 10752310, 10702046])
|
||||
for item in v:
|
||||
video_id = item.uri.split("/")[-1]
|
||||
print(f" - {item.name} : {video_id} ")
|
||||
# from IPython import embed; embed()
|
||||
# s
|
||||
# vi=cl.get_video_info("475353425")
|
||||
# print(json_to_yaml(vi))
|
||||
# cl.download("475353425", "/tmp/475353425.mp4")
|
177
herolib/clients/vimeo/model_video.py
Normal file
177
herolib/clients/vimeo/model_video.py
Normal file
@@ -0,0 +1,177 @@
|
||||
from dataclasses import dataclass, field
|
||||
from typing import List, Optional, Dict, Any
|
||||
from dataclasses_json import dataclass_json
|
||||
import json
|
||||
import yaml
|
||||
|
||||
def json_to_yaml(json_data):
|
||||
# If the input is a JSON string, parse it into a Python dictionary
|
||||
if isinstance(json_data, str):
|
||||
json_data = json.loads(json_data)
|
||||
|
||||
# Convert the dictionary to a YAML formatted string
|
||||
yaml_data = yaml.dump(json_data, sort_keys=False, default_flow_style=False)
|
||||
|
||||
return yaml_data
|
||||
|
||||
|
||||
@dataclass_json
|
||||
@dataclass
|
||||
class Size:
|
||||
width: int
|
||||
height: int
|
||||
link: str
|
||||
link_with_play_button: Optional[str] = None
|
||||
|
||||
@dataclass_json
|
||||
@dataclass
|
||||
class Pictures:
|
||||
uri: str
|
||||
active: bool
|
||||
type: str
|
||||
base_link: str
|
||||
sizes: List[Size]
|
||||
resource_key: str
|
||||
default_picture: bool
|
||||
|
||||
@dataclass_json
|
||||
@dataclass
|
||||
class Embed:
|
||||
html: str
|
||||
badges: Dict[str, Any]
|
||||
interactive: bool
|
||||
buttons: Dict[str, bool]
|
||||
logos: Dict[str, Any]
|
||||
play_button: Dict[str, Any]
|
||||
title: Dict[str, Any]
|
||||
end_screen: List[Any]
|
||||
playbar: bool
|
||||
quality_selector: Optional[str]
|
||||
pip: bool
|
||||
autopip: bool
|
||||
volume: bool
|
||||
color: str
|
||||
colors: Dict[str, str]
|
||||
event_schedule: bool
|
||||
has_cards: bool
|
||||
outro_type: str
|
||||
show_timezone: bool
|
||||
cards: List[Any]
|
||||
airplay: bool
|
||||
audio_tracks: bool
|
||||
chapters: bool
|
||||
chromecast: bool
|
||||
closed_captions: bool
|
||||
transcript: bool
|
||||
ask_ai: bool
|
||||
uri: Optional[str]
|
||||
email_capture_form: Optional[str]
|
||||
speed: bool
|
||||
|
||||
@dataclass_json
|
||||
@dataclass
|
||||
class Uploader:
|
||||
pictures: Pictures
|
||||
|
||||
@dataclass_json
|
||||
@dataclass
|
||||
class User:
|
||||
uri: str
|
||||
name: str
|
||||
link: str
|
||||
capabilities: Dict[str, bool]
|
||||
location: str
|
||||
gender: str
|
||||
bio: str
|
||||
short_bio: str
|
||||
created_time: str
|
||||
pictures: Pictures
|
||||
websites: List[Dict[str, Optional[str]]]
|
||||
#metadata: Dict[str, Any]
|
||||
location_details: Dict[str, Optional[Any]]
|
||||
skills: List[Any]
|
||||
available_for_hire: bool
|
||||
can_work_remotely: bool
|
||||
preferences: Dict[str, Any]
|
||||
content_filter: List[str]
|
||||
upload_quota: Dict[str, Any]
|
||||
resource_key: str
|
||||
account: str
|
||||
|
||||
@dataclass_json
|
||||
@dataclass
|
||||
class VideoInfo:
|
||||
uri: str
|
||||
name: str
|
||||
description: Optional[str]
|
||||
type: str
|
||||
link: str
|
||||
player_embed_url: str
|
||||
duration: int
|
||||
width: int
|
||||
height: int
|
||||
#embed: Embed
|
||||
created_time: str
|
||||
modified_time: str
|
||||
release_time: str
|
||||
content_rating: List[str]
|
||||
content_rating_class: str
|
||||
rating_mod_locked: bool
|
||||
license: Optional[str]
|
||||
privacy: Dict[str, Any]
|
||||
pictures: Pictures
|
||||
tags: List[Any]
|
||||
stats: Dict[str, int]
|
||||
categories: List[Any]
|
||||
uploader: Uploader
|
||||
#metadata: Dict[str, Any]
|
||||
manage_link: str
|
||||
#user: Optional[User]
|
||||
last_user_action_event_date: Optional[str]
|
||||
parent_folder: Optional[Dict[str, Any]]
|
||||
review_page: Optional[Dict[str, Any]]
|
||||
files: Optional[List[Dict[str, Any]]]
|
||||
download: Optional[List[Dict[str, Any]]]
|
||||
app: Optional[Dict[str, str]]
|
||||
play: Optional[Dict[str, Any]]
|
||||
status: str
|
||||
resource_key: str
|
||||
upload: Optional[Dict[str, Optional[str]]]
|
||||
transcode: Dict[str, str]
|
||||
is_playable: bool
|
||||
has_audio: bool
|
||||
|
||||
|
||||
def video_model_load(json_data:str,dojsonload:bool=True) -> VideoInfo:
|
||||
|
||||
if dojsonload:
|
||||
json_dict = json.loads(json_data)
|
||||
else:
|
||||
json_dict = json_data
|
||||
|
||||
json_dict.pop('metadata', {})
|
||||
json_dict.pop('embed', {})
|
||||
json_dict.pop('user', {})
|
||||
json_dict.pop('websites', {})
|
||||
# if 'user' in json_dict:
|
||||
# json_dict['user'].pop('metadata', None)
|
||||
# if 'websites' in json_dict:
|
||||
# json_dict['websites'].pop('metadata', None)
|
||||
|
||||
|
||||
json_data_cleaned = json.dumps(json_dict)
|
||||
|
||||
video_object = VideoInfo.from_json(json_data_cleaned)
|
||||
|
||||
return video_object
|
||||
|
||||
|
||||
def videos_model_load(json_data:str) -> List[VideoInfo]:
|
||||
json_list = json.loads(json_data)
|
||||
json_list2= list()
|
||||
|
||||
for item in json_list["data"]:
|
||||
d=video_model_load(item,dojsonload=False)
|
||||
json_list2.append(d)
|
||||
|
||||
return json_list2
|
0
herolib/clients/whisper/__init__.py
Normal file
0
herolib/clients/whisper/__init__.py
Normal file
107
herolib/clients/whisper/convert.py
Normal file
107
herolib/clients/whisper/convert.py
Normal file
@@ -0,0 +1,107 @@
|
||||
import os
|
||||
from pydub import AudioSegment
|
||||
import whisper
|
||||
import moviepy.editor as mp
|
||||
import nltk
|
||||
from nltk.tokenize import sent_tokenize, word_tokenize
|
||||
|
||||
# Download necessary NLTK data
|
||||
nltk.download('punkt', quiet=True)
|
||||
|
||||
class Convertor:
|
||||
def __init__(self, max_chars_per_part=4000,context:str = "main"):
|
||||
self.max_chars_per_part = max_chars_per_part
|
||||
self.context = context
|
||||
|
||||
@classmethod
|
||||
def new(cls, max_chars_per_part=4000):
|
||||
return cls(max_chars_per_part)
|
||||
|
||||
def process(self, path: str):
|
||||
if path.lower().endswith(('.mp4', '.avi', '.mov')): # Video files
|
||||
return self.process_video(path)
|
||||
elif path.lower().endswith(('.mp3', '.wav', '.ogg')): # Audio files
|
||||
return self.process_audio(path)
|
||||
else:
|
||||
raise ValueError("Unsupported file format")
|
||||
|
||||
def process_video(self, video_path: str):
|
||||
# Extract audio from video
|
||||
video = mp.VideoFileClip(video_path)
|
||||
audio_path = video_path.rsplit('.', 1)[0] + '.wav'
|
||||
video.audio.write_audiofile(audio_path)
|
||||
video.close()
|
||||
return audio_path
|
||||
|
||||
def process_audio(self, audio_path: str):
|
||||
# Convert to WAV format if necessary
|
||||
wav_path = audio_path.rsplit('.', 1)[0] + '.wav'
|
||||
if not audio_path.lower().endswith('.wav'):
|
||||
audio = AudioSegment.from_file(audio_path)
|
||||
audio.export(wav_path, format='wav')
|
||||
else:
|
||||
wav_path = audio_path
|
||||
|
||||
def split_text(self, text):
|
||||
parts = []
|
||||
current_part = ""
|
||||
paragraphs = text.split('\n\n')
|
||||
|
||||
for paragraph in paragraphs:
|
||||
sentences = sent_tokenize(paragraph)
|
||||
for sentence in sentences:
|
||||
if len(current_part) + len(sentence) < self.max_chars_per_part:
|
||||
current_part += sentence + ' '
|
||||
else:
|
||||
if current_part:
|
||||
parts.append(current_part.strip())
|
||||
current_part = sentence + ' '
|
||||
|
||||
# Add a paragraph break if it doesn't exceed the limit
|
||||
if len(current_part) + 2 < self.max_chars_per_part:
|
||||
current_part += '\n\n'
|
||||
else:
|
||||
parts.append(current_part.strip())
|
||||
current_part = '\n\n'
|
||||
|
||||
if current_part:
|
||||
parts.append(current_part.strip())
|
||||
|
||||
return parts
|
||||
|
||||
def find_natural_pause(self, text):
|
||||
words = word_tokenize(text)
|
||||
total_words = len(words)
|
||||
mid_point = total_words // 2
|
||||
|
||||
# Look for punctuation near the middle
|
||||
for i in range(mid_point, total_words):
|
||||
if words[i] in '.!?':
|
||||
return ' '.join(words[:i+1]), ' '.join(words[i+1:])
|
||||
|
||||
# If no punctuation found, split at the nearest space to the middle
|
||||
return ' '.join(words[:mid_point]), ' '.join(words[mid_point:])
|
||||
|
||||
def write_to_file(self, parts, output_path):
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
for i, part in enumerate(parts, 1):
|
||||
f.write(f"Part {i}:\n\n")
|
||||
f.write(part)
|
||||
f.write("\n\n")
|
||||
if i < len(parts):
|
||||
f.write("-" * 50 + "\n\n")
|
||||
|
||||
|
||||
# Usage example:
|
||||
if __name__ == "__main__":
|
||||
processor = Convertor.new()
|
||||
item = "/Users/despiegk1/Documents/Zoom/2024-07-16 16.42.50 Kristof De Spiegeleer's Personal Meeting Room/video1720369800.mp4"
|
||||
transcription_parts = processor.process(item)
|
||||
|
||||
processor.write_to_file(transcription_parts, output_file)
|
||||
|
||||
print(f"Transcription split into {len(transcription_parts)} parts:")
|
||||
for i, part in enumerate(transcription_parts, 1):
|
||||
print(f"Part {i}:")
|
||||
print(part)
|
||||
print("-" * 50)
|
118
herolib/clients/whisper/whisper.py
Normal file
118
herolib/clients/whisper/whisper.py
Normal file
@@ -0,0 +1,118 @@
|
||||
import os
|
||||
from pydub import AudioSegment
|
||||
import whisper
|
||||
import moviepy.editor as mp
|
||||
import nltk
|
||||
from nltk.tokenize import sent_tokenize, word_tokenize
|
||||
|
||||
# Download necessary NLTK data
|
||||
nltk.download('punkt', quiet=True)
|
||||
|
||||
class MediaProcessor:
|
||||
def __init__(self, max_chars_per_part=4000):
|
||||
self.model = whisper.load_model("base.en")
|
||||
#self.model = whisper.load_model("medium.en")
|
||||
self.max_chars_per_part = max_chars_per_part
|
||||
|
||||
@classmethod
|
||||
def new(cls, max_chars_per_part=4000):
|
||||
return cls(max_chars_per_part)
|
||||
|
||||
def process(self, path: str):
|
||||
if path.lower().endswith(('.mp4', '.avi', '.mov')): # Video files
|
||||
return self.process_video(path)
|
||||
elif path.lower().endswith(('.mp3', '.wav', '.ogg')): # Audio files
|
||||
return self.process_audio(path)
|
||||
else:
|
||||
raise ValueError("Unsupported file format")
|
||||
|
||||
def process_video(self, video_path: str):
|
||||
# Extract audio from video
|
||||
video = mp.VideoFileClip(video_path)
|
||||
audio_path = video_path.rsplit('.', 1)[0] + '.wav'
|
||||
video.audio.write_audiofile(audio_path)
|
||||
video.close()
|
||||
|
||||
# Now process the extracted audio
|
||||
return self.process_audio(audio_path)
|
||||
|
||||
def process_audio(self, audio_path: str):
|
||||
# Convert to WAV format if necessary
|
||||
wav_path = audio_path.rsplit('.', 1)[0] + '.wav'
|
||||
if not audio_path.lower().endswith('.wav'):
|
||||
audio = AudioSegment.from_file(audio_path)
|
||||
audio.export(wav_path, format='wav')
|
||||
else:
|
||||
wav_path = audio_path
|
||||
|
||||
# Transcribe audio using Whisper
|
||||
result = self.model.transcribe(wav_path)
|
||||
transcription = result["text"]
|
||||
|
||||
# Split the transcription into parts
|
||||
return self.split_text(transcription)
|
||||
|
||||
def split_text(self, text):
|
||||
parts = []
|
||||
current_part = ""
|
||||
paragraphs = text.split('\n\n')
|
||||
|
||||
for paragraph in paragraphs:
|
||||
sentences = sent_tokenize(paragraph)
|
||||
for sentence in sentences:
|
||||
if len(current_part) + len(sentence) < self.max_chars_per_part:
|
||||
current_part += sentence + ' '
|
||||
else:
|
||||
if current_part:
|
||||
parts.append(current_part.strip())
|
||||
current_part = sentence + ' '
|
||||
|
||||
# Add a paragraph break if it doesn't exceed the limit
|
||||
if len(current_part) + 2 < self.max_chars_per_part:
|
||||
current_part += '\n\n'
|
||||
else:
|
||||
parts.append(current_part.strip())
|
||||
current_part = '\n\n'
|
||||
|
||||
if current_part:
|
||||
parts.append(current_part.strip())
|
||||
|
||||
return parts
|
||||
|
||||
def find_natural_pause(self, text):
|
||||
words = word_tokenize(text)
|
||||
total_words = len(words)
|
||||
mid_point = total_words // 2
|
||||
|
||||
# Look for punctuation near the middle
|
||||
for i in range(mid_point, total_words):
|
||||
if words[i] in '.!?':
|
||||
return ' '.join(words[:i+1]), ' '.join(words[i+1:])
|
||||
|
||||
# If no punctuation found, split at the nearest space to the middle
|
||||
return ' '.join(words[:mid_point]), ' '.join(words[mid_point:])
|
||||
|
||||
def write_to_file(self, parts, output_path):
|
||||
with open(output_path, 'w', encoding='utf-8') as f:
|
||||
for i, part in enumerate(parts, 1):
|
||||
f.write(f"Part {i}:\n\n")
|
||||
f.write(part)
|
||||
f.write("\n\n")
|
||||
if i < len(parts):
|
||||
f.write("-" * 50 + "\n\n")
|
||||
|
||||
|
||||
# Usage example:
|
||||
if __name__ == "__main__":
|
||||
processor = MediaProcessor.new(max_chars_per_part=10000)
|
||||
output_file = "/Users/despiegk1/Documents/transcription3.md"
|
||||
item = "/Users/despiegk1/Documents/Zoom/2024-07-16 16.42.50 Kristof De Spiegeleer's Personal Meeting Room/video1720369800.mp4"
|
||||
transcription_parts = processor.process(item)
|
||||
|
||||
processor.write_to_file(transcription_parts, output_file)
|
||||
|
||||
print(f"Transcription split into {len(transcription_parts)} parts:")
|
||||
for i, part in enumerate(transcription_parts, 1):
|
||||
print(f"Part {i}:")
|
||||
print(part)
|
||||
print("-" * 50)
|
0
herolib/clients/wireless/__init__.py
Normal file
0
herolib/clients/wireless/__init__.py
Normal file
313
herolib/clients/wireless/wigle_net.py
Normal file
313
herolib/clients/wireless/wigle_net.py
Normal file
@@ -0,0 +1,313 @@
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
import redis
|
||||
import requests
|
||||
|
||||
API_URL = "https://api.wigle.net/api/v2/network/search"
|
||||
REDIS_CACHE_EXPIRY = timedelta(hours=1)
|
||||
API_RATE_LIMIT = 30 # seconds between requests
|
||||
|
||||
# Initialize Redis connection
|
||||
redis_client = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True)
|
||||
|
||||
# Track last API request time (initialized to allow immediate first request)
|
||||
_last_request_time = time.time() - API_RATE_LIMIT
|
||||
|
||||
|
||||
class WigleError(Exception):
|
||||
"""Custom exception for Wigle-related errors"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class NetworkType(str, Enum):
|
||||
"""Network types supported by Wigle API"""
|
||||
|
||||
WIFI = "WIFI"
|
||||
BT = "BT"
|
||||
CELL = "CELL"
|
||||
|
||||
|
||||
class Encryption(str, Enum):
|
||||
"""WiFi encryption types"""
|
||||
|
||||
NONE = "None"
|
||||
WEP = "WEP"
|
||||
WPA = "WPA"
|
||||
WPA2 = "WPA2"
|
||||
WPA3 = "WPA3"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
@dataclass
|
||||
class Location:
|
||||
"""Represents a wireless network location with all available Wigle API fields"""
|
||||
|
||||
ssid: str
|
||||
latitude: float
|
||||
longitude: float
|
||||
last_update: Optional[datetime]
|
||||
encryption: Optional[str] = None
|
||||
network_type: Optional[str] = None
|
||||
channel: Optional[int] = None
|
||||
frequency: Optional[float] = None
|
||||
qos: Optional[int] = None
|
||||
transid: Optional[str] = None
|
||||
firsttime: Optional[datetime] = None
|
||||
lasttime: Optional[datetime] = None
|
||||
country_code: Optional[str] = None
|
||||
city: Optional[str] = None
|
||||
region: Optional[str] = None
|
||||
house_number: Optional[str] = None
|
||||
road: Optional[str] = None
|
||||
address: Optional[str] = None
|
||||
|
||||
|
||||
def get_wigle_auth() -> str:
|
||||
"""Get Wigle authentication token from environment variable"""
|
||||
wigle_auth = os.getenv("WIGLE")
|
||||
if not wigle_auth:
|
||||
raise WigleError("WIGLE environment variable not set. Format should be: 'AIDxxx:yyy'")
|
||||
return wigle_auth
|
||||
|
||||
|
||||
def enforce_rate_limit():
|
||||
"""Enforce API rate limit by sleeping if needed, showing countdown"""
|
||||
global _last_request_time
|
||||
current_time = time.time()
|
||||
time_since_last_request = current_time - _last_request_time
|
||||
|
||||
if time_since_last_request < API_RATE_LIMIT:
|
||||
sleep_time = API_RATE_LIMIT - time_since_last_request
|
||||
print(f"\nRate limit: waiting {sleep_time:.0f} seconds", end="", flush=True)
|
||||
|
||||
# Show countdown
|
||||
for remaining in range(int(sleep_time), 0, -1):
|
||||
time.sleep(1)
|
||||
print(f"\rRate limit: waiting {remaining:2d} seconds", end="", flush=True)
|
||||
|
||||
print("\rRate limit: continuing... ") # Clear the line
|
||||
|
||||
_last_request_time = time.time()
|
||||
|
||||
|
||||
def search_networks(
|
||||
*,
|
||||
# Location filters
|
||||
latitude_north: Optional[float] = None,
|
||||
latitude_south: Optional[float] = None,
|
||||
longitude_east: Optional[float] = None,
|
||||
longitude_west: Optional[float] = None,
|
||||
# Network filters
|
||||
ssid: Optional[str] = None,
|
||||
ssidlike: Optional[str] = None,
|
||||
network_type: Optional[NetworkType] = None,
|
||||
encryption: Optional[Encryption] = None,
|
||||
# Time filters
|
||||
on_since: Optional[datetime] = None,
|
||||
last_update: Optional[datetime] = None,
|
||||
# Result control
|
||||
results_per_page: int = 100,
|
||||
search_after: Optional[str] = None,
|
||||
# Other filters
|
||||
freenet: Optional[bool] = None,
|
||||
paynet: Optional[bool] = None,
|
||||
show_query: bool = False,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Search for networks using the Wigle API with full parameter support and Redis caching.
|
||||
Rate limited to one request per minute.
|
||||
|
||||
Args:
|
||||
latitude_north: Northern boundary of search box
|
||||
latitude_south: Southern boundary of search box
|
||||
longitude_east: Eastern boundary of search box
|
||||
longitude_west: Western boundary of search box
|
||||
ssid: Exact SSID match
|
||||
ssidlike: SSID wildcard match
|
||||
network_type: Filter by network type (WIFI/BT/CELL)
|
||||
encryption: Filter by encryption type
|
||||
on_since: Only show networks seen on or after date
|
||||
last_update: Only show networks updated since date
|
||||
results_per_page: Number of results per page (max 100)
|
||||
search_after: Token for getting next batch of results
|
||||
freenet: Show only free networks
|
||||
paynet: Show only pay networks
|
||||
show_query: Return query bounds without results
|
||||
|
||||
Returns:
|
||||
Dictionary containing search results and metadata including searchAfter token
|
||||
|
||||
Raises:
|
||||
WigleError: If the WIGLE environment variable is not set or API request fails
|
||||
"""
|
||||
# https://api.wigle.net/api/v2/network/search?onlymine=false&encryption=None&freenet=false&paynet=false
|
||||
try:
|
||||
# Build cache key from all parameters
|
||||
params = locals()
|
||||
cache_key = f"wigle:search:{json.dumps(params, default=str, sort_keys=True)}"
|
||||
|
||||
cached_result = redis_client.get(cache_key)
|
||||
if cached_result:
|
||||
return json.loads(cached_result)
|
||||
|
||||
# Enforce rate limit before making request
|
||||
enforce_rate_limit()
|
||||
|
||||
# Build API parameters
|
||||
api_params = {
|
||||
"onlymine": "false",
|
||||
"resultsPerPage": results_per_page,
|
||||
}
|
||||
|
||||
# Add optional parameters if provided
|
||||
if latitude_north is not None:
|
||||
api_params["latrange1"] = latitude_south
|
||||
api_params["latrange2"] = latitude_north
|
||||
api_params["longrange1"] = longitude_west
|
||||
api_params["longrange2"] = longitude_east
|
||||
|
||||
if ssid:
|
||||
api_params["ssid"] = ssid
|
||||
if ssidlike:
|
||||
api_params["ssidlike"] = ssidlike
|
||||
if network_type:
|
||||
api_params["netid"] = network_type.value
|
||||
if encryption:
|
||||
api_params["encryption"] = encryption.value
|
||||
else:
|
||||
api_params["encryption"] = "None"
|
||||
if on_since:
|
||||
api_params["onSince"] = on_since.strftime("%Y%m%d")
|
||||
if last_update:
|
||||
api_params["lastupdt"] = last_update.strftime("%Y%m%d")
|
||||
if freenet is not None:
|
||||
api_params["freenet"] = str(freenet).lower()
|
||||
if paynet is not None:
|
||||
api_params["paynet"] = str(paynet).lower()
|
||||
if search_after:
|
||||
api_params["searchAfter"] = search_after
|
||||
if show_query:
|
||||
api_params["showQuery"] = str(show_query).lower()
|
||||
|
||||
# Make API request
|
||||
wigle_auth = get_wigle_auth()
|
||||
headers = {"Authorization": f"Basic {wigle_auth}"}
|
||||
response = requests.get(API_URL, params=api_params, headers=headers)
|
||||
response.raise_for_status()
|
||||
result = response.json()
|
||||
|
||||
print(result)
|
||||
|
||||
# Cache the result
|
||||
redis_client.setex(cache_key, int(REDIS_CACHE_EXPIRY.total_seconds()), json.dumps(result))
|
||||
|
||||
return result
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
raise WigleError(f"API request failed: {str(e)}")
|
||||
|
||||
|
||||
def parse_network_to_location(network: Dict[str, Any]) -> Location:
|
||||
"""Convert a network result from Wigle API to a Location object"""
|
||||
# Parse dates if present
|
||||
last_update = None
|
||||
firsttime = None
|
||||
lasttime = None
|
||||
|
||||
if network.get("lastupdt"):
|
||||
try:
|
||||
last_update = datetime.strptime(network["lastupdt"], "%Y-%m-%d %H:%M:%S")
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if network.get("firsttime"):
|
||||
try:
|
||||
firsttime = datetime.strptime(network["firsttime"], "%Y-%m-%d %H:%M:%S")
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if network.get("lasttime"):
|
||||
try:
|
||||
lasttime = datetime.strptime(network["lasttime"], "%Y-%m-%d %H:%M:%S")
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
return Location(
|
||||
ssid=network["ssid"],
|
||||
latitude=float(network["trilat"]),
|
||||
longitude=float(network["trilong"]),
|
||||
last_update=last_update,
|
||||
encryption=network.get("encryption"),
|
||||
network_type=network.get("type"),
|
||||
channel=network.get("channel"),
|
||||
frequency=network.get("frequency"),
|
||||
qos=network.get("qos"),
|
||||
transid=network.get("transid"),
|
||||
firsttime=firsttime,
|
||||
lasttime=lasttime,
|
||||
country_code=network.get("country"),
|
||||
city=network.get("city"),
|
||||
region=network.get("region"),
|
||||
house_number=network.get("housenumber"),
|
||||
road=network.get("road"),
|
||||
address=network.get("address"),
|
||||
)
|
||||
|
||||
|
||||
def get_all() -> List[Location]:
|
||||
"""Search for OpenRoaming networks and return list of locations.
|
||||
Rate limited to one request per minute, including pagination requests.
|
||||
|
||||
Returns:
|
||||
List[Location]: List of found network locations
|
||||
|
||||
Raises:
|
||||
WigleError: If the WIGLE environment variable is not set or API request fails
|
||||
"""
|
||||
ssid_names = ["Adentro OpenRoaming", "OpenRoaming", "Passpoint", "PasspointAruba", "Cellular Wi-Fi Passthrough", "WBA_OpenRoaming"]
|
||||
locations: List[Location] = []
|
||||
|
||||
for name in ssid_names:
|
||||
try:
|
||||
search_after = None
|
||||
while True:
|
||||
results = search_networks(
|
||||
ssid=name, encryption=Encryption.NONE, network_type=NetworkType.WIFI, results_per_page=100, search_after=search_after
|
||||
)
|
||||
|
||||
if not results or not results.get("results"):
|
||||
break
|
||||
|
||||
for network in results["results"]:
|
||||
locations.append(parse_network_to_location(network))
|
||||
|
||||
# Get searchAfter token for next batch
|
||||
search_after = results.get("searchAfter")
|
||||
if not search_after:
|
||||
break
|
||||
|
||||
except WigleError as e:
|
||||
raise WigleError(f"Error searching for {name}: {str(e)}")
|
||||
|
||||
print(f"Found {len(locations)} OpenRoaming network locations")
|
||||
return locations
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
locations = get_all()
|
||||
for loc in locations:
|
||||
print(f"SSID: {loc.ssid}")
|
||||
print(f"Location: ({loc.latitude}, {loc.longitude})")
|
||||
print(f"Network Type: {loc.network_type or 'N/A'}")
|
||||
print(f"Encryption: {loc.encryption or 'N/A'}")
|
||||
print(f"Last Update: {loc.last_update or 'N/A'}")
|
||||
if loc.address:
|
||||
print(f"Address: {loc.address}")
|
||||
print("-" * 50)
|
Reference in New Issue
Block a user