Spaces:
Running
Running
# Utils.py | |
######################################### | |
# General Utilities Library | |
# This library is used to hold random utilities used by various other libraries. | |
# | |
#### | |
#################### | |
# Function List | |
# | |
# 1. extract_text_from_segments(segments: List[Dict]) -> str | |
# 2. download_file(url, dest_path, expected_checksum=None, max_retries=3, delay=5) | |
# 3. verify_checksum(file_path, expected_checksum) | |
# 4. create_download_directory(title) | |
# 5. sanitize_filename(filename) | |
# 6. normalize_title(title) | |
# 7. | |
# | |
# | |
# | |
#################### | |
# Import necessary libraries | |
import configparser | |
import hashlib | |
import json | |
import logging | |
import os | |
import re | |
import time | |
from datetime import timedelta | |
from urllib.parse import urlparse, parse_qs, urlencode, urlunparse | |
import requests | |
import unicodedata | |
from tqdm import tqdm | |
####################################################################################################################### | |
# Function Definitions | |
# | |
def extract_text_from_segments(segments): | |
logging.debug(f"Segments received: {segments}") | |
logging.debug(f"Type of segments: {type(segments)}") | |
def extract_text_recursive(data): | |
if isinstance(data, dict): | |
for key, value in data.items(): | |
if key == 'Text': | |
return value | |
elif isinstance(value, (dict, list)): | |
result = extract_text_recursive(value) | |
if result: | |
return result | |
elif isinstance(data, list): | |
return ' '.join(filter(None, [extract_text_recursive(item) for item in data])) | |
return None | |
text = extract_text_recursive(segments) | |
if text: | |
return text.strip() | |
else: | |
logging.error(f"Unable to extract text from segments: {segments}") | |
return "Error: Unable to extract transcription" | |
def import_data(file): | |
# Implement this function to import data from a file | |
pass | |
# | |
# | |
####################### | |
# Temp file cleanup | |
# | |
# Global list to keep track of downloaded files | |
downloaded_files = [] | |
def cleanup_downloads(): | |
"""Function to clean up downloaded files when the server exits.""" | |
for file_path in downloaded_files: | |
try: | |
if os.path.exists(file_path): | |
os.remove(file_path) | |
print(f"Cleaned up file: {file_path}") | |
except Exception as e: | |
print(f"Error cleaning up file {file_path}: {e}") | |
# | |
# | |
####################################################################################################################### | |
####################################################################################################################### | |
# Config loading | |
# | |
def load_comprehensive_config(): | |
# Get the directory of the current script | |
current_dir = os.path.dirname(os.path.abspath(__file__)) | |
# Go up one level to the project root directory | |
project_root = os.path.dirname(current_dir) | |
# Construct the path to the config file in the project root directory | |
config_path = os.path.join(project_root, 'config.txt') | |
# Create a ConfigParser object | |
config = configparser.ConfigParser() | |
# Read the configuration file | |
files_read = config.read(config_path) | |
if not files_read: | |
raise FileNotFoundError(f"Config file not found at {config_path}") | |
return config | |
# FIXME - update to include prompt path in return statement | |
def load_and_log_configs(): | |
try: | |
config = load_comprehensive_config() | |
if config is None: | |
logging.error("Config is None, cannot proceed") | |
return None | |
# API Keys | |
anthropic_api_key = config.get('API', 'anthropic_api_key', fallback=None) | |
logging.debug( | |
f"Loaded Anthropic API Key: {anthropic_api_key[:5]}...{anthropic_api_key[-5:] if anthropic_api_key else None}") | |
cohere_api_key = config.get('API', 'cohere_api_key', fallback=None) | |
logging.debug( | |
f"Loaded Cohere API Key: {cohere_api_key[:5]}...{cohere_api_key[-5:] if cohere_api_key else None}") | |
groq_api_key = config.get('API', 'groq_api_key', fallback=None) | |
logging.debug(f"Loaded Groq API Key: {groq_api_key[:5]}...{groq_api_key[-5:] if groq_api_key else None}") | |
openai_api_key = config.get('API', 'openai_api_key', fallback=None) | |
logging.debug( | |
f"Loaded OpenAI API Key: {openai_api_key[:5]}...{openai_api_key[-5:] if openai_api_key else None}") | |
huggingface_api_key = config.get('API', 'huggingface_api_key', fallback=None) | |
logging.debug( | |
f"Loaded HuggingFace API Key: {huggingface_api_key[:5]}...{huggingface_api_key[-5:] if huggingface_api_key else None}") | |
openrouter_api_key = config.get('API', 'openrouter_api_key', fallback=None) | |
logging.debug( | |
f"Loaded OpenRouter API Key: {openrouter_api_key[:5]}...{openrouter_api_key[-5:] if openrouter_api_key else None}") | |
deepseek_api_key = config.get('API', 'deepseek_api_key', fallback=None) | |
logging.debug( | |
f"Loaded DeepSeek API Key: {deepseek_api_key[:5]}...{deepseek_api_key[-5:] if deepseek_api_key else None}") | |
mistral_api_key = config.get('API', 'mistral_api_key', fallback=None) | |
logging.debug( | |
f"Loaded Mistral API Key: {mistral_api_key[:5]}...{mistral_api_key[-5:] if mistral_api_key else None}") | |
# Models | |
anthropic_model = config.get('API', 'anthropic_model', fallback='claude-3-sonnet-20240229') | |
cohere_model = config.get('API', 'cohere_model', fallback='command-r-plus') | |
groq_model = config.get('API', 'groq_model', fallback='llama3-70b-8192') | |
openai_model = config.get('API', 'openai_model', fallback='gpt-4-turbo') | |
huggingface_model = config.get('API', 'huggingface_model', fallback='CohereForAI/c4ai-command-r-plus') | |
openrouter_model = config.get('API', 'openrouter_model', fallback='microsoft/wizardlm-2-8x22b') | |
deepseek_model = config.get('API', 'deepseek_model', fallback='deepseek-chat') | |
mistral_model = config.get('API', 'mistral_model', fallback='mistral-large-latest') | |
logging.debug(f"Loaded Anthropic Model: {anthropic_model}") | |
logging.debug(f"Loaded Cohere Model: {cohere_model}") | |
logging.debug(f"Loaded Groq Model: {groq_model}") | |
logging.debug(f"Loaded OpenAI Model: {openai_model}") | |
logging.debug(f"Loaded HuggingFace Model: {huggingface_model}") | |
logging.debug(f"Loaded OpenRouter Model: {openrouter_model}") | |
logging.debug(f"Loaded Deepseek Model: {deepseek_model}") | |
logging.debug(f"Loaded Mistral Model: {mistral_model}") | |
# Local-Models | |
kobold_api_ip = config.get('Local-API', 'kobold_api_IP', fallback='http://127.0.0.1:5000/api/v1/generate') | |
kobold_api_key = config.get('Local-API', 'kobold_api_key', fallback='') | |
llama_api_IP = config.get('Local-API', 'llama_api_IP', fallback='http://127.0.0.1:8080/v1/chat/completions') | |
llama_api_key = config.get('Local-API', 'llama_api_key', fallback='') | |
ooba_api_IP = config.get('Local-API', 'ooba_api_IP', fallback='http://127.0.0.1:5000/v1/chat/completions') | |
ooba_api_key = config.get('Local-API', 'ooba_api_key', fallback='') | |
tabby_api_IP = config.get('Local-API', 'tabby_api_IP', fallback='http://127.0.0.1:5000/api/v1/generate') | |
tabby_api_key = config.get('Local-API', 'tabby_api_key', fallback=None) | |
tabby_model = config.get('models', 'tabby_model', fallback=None) | |
vllm_api_url = config.get('Local-API', 'vllm_api_IP', fallback='http://127.0.0.1:500/api/v1/chat/completions') | |
vllm_api_key = config.get('Local-API', 'vllm_api_key', fallback=None) | |
vllm_model = config.get('Local-API', 'vllm_model', fallback=None) | |
ollama_api_url = config.get('Local-API', 'ollama_api_IP', fallback='http://127.0.0.1:11434/api/generate') | |
ollama_api_key = config.get('Local-API', 'ollama_api_key', fallback=None) | |
ollama_model = config.get('Local-API', 'ollama_model', fallback=None) | |
aphrodite_api_url = config.get('Local-API', 'aphrodite_api_IP', fallback='http://127.0.0.1:8080/v1/chat/completions') | |
aphrodite_api_key = config.get('Local-API', 'aphrodite_api_key', fallback='') | |
logging.debug(f"Loaded Kobold API IP: {kobold_api_ip}") | |
logging.debug(f"Loaded Llama API IP: {llama_api_IP}") | |
logging.debug(f"Loaded Ooba API IP: {ooba_api_IP}") | |
logging.debug(f"Loaded Tabby API IP: {tabby_api_IP}") | |
logging.debug(f"Loaded VLLM API URL: {vllm_api_url}") | |
# Retrieve output paths from the configuration file | |
output_path = config.get('Paths', 'output_path', fallback='results') | |
logging.debug(f"Output path set to: {output_path}") | |
# Retrieve processing choice from the configuration file | |
processing_choice = config.get('Processing', 'processing_choice', fallback='cpu') | |
logging.debug(f"Processing choice set to: {processing_choice}") | |
# Prompts - FIXME | |
prompt_path = config.get('Prompts', 'prompt_path', fallback='prompts.db') | |
return { | |
'api_keys': { | |
'anthropic': anthropic_api_key, | |
'cohere': cohere_api_key, | |
'groq': groq_api_key, | |
'openai': openai_api_key, | |
'huggingface': huggingface_api_key, | |
'openrouter': openrouter_api_key, | |
'deepseek': deepseek_api_key, | |
'mistral': mistral_api_key, | |
'kobold': kobold_api_key, | |
'llama': llama_api_key, | |
'ooba': ooba_api_key, | |
'tabby': tabby_api_key, | |
'vllm': vllm_api_key, | |
'ollama': ollama_api_key | |
}, | |
'models': { | |
'anthropic': anthropic_model, | |
'cohere': cohere_model, | |
'groq': groq_model, | |
'openai': openai_model, | |
'huggingface': huggingface_model, | |
'openrouter': openrouter_model, | |
'deepseek': deepseek_model, | |
'mistral': mistral_model, | |
'vllm': vllm_model, | |
'tabby': tabby_model, | |
'ollama': ollama_model | |
}, | |
'local_api_ip': { | |
'kobold': kobold_api_ip, | |
'llama': llama_api_IP, | |
'ooba': ooba_api_IP, | |
'tabby': tabby_api_IP, | |
'vllm': vllm_api_url, | |
'ollama': ollama_api_url, | |
'aphrodite': aphrodite_api_url | |
}, | |
'output_path': output_path, | |
'processing_choice': processing_choice | |
} | |
except Exception as e: | |
logging.error(f"Error loading config: {str(e)}") | |
return None | |
# | |
# End of Config loading | |
####################################################################################################################### | |
####################################################################################################################### | |
# | |
# Prompt Handling Functions | |
# | |
# End of Prompt Handling Functions | |
### ############################################################################################################# | |
####################################################################################################################### | |
# | |
# Misc-Functions | |
# Log file | |
# logging.basicConfig(filename='debug-runtime.log', encoding='utf-8', level=logging.DEBUG) | |
def format_metadata_as_text(metadata): | |
if not metadata: | |
return "No metadata available" | |
formatted_text = "Video Metadata:\n" | |
for key, value in metadata.items(): | |
if value is not None: | |
if isinstance(value, list): | |
# Join list items with commas | |
formatted_value = ", ".join(str(item) for item in value) | |
elif key == 'upload_date' and len(str(value)) == 8: | |
# Format date as YYYY-MM-DD | |
formatted_value = f"{value[:4]}-{value[4:6]}-{value[6:]}" | |
elif key in ['view_count', 'like_count']: | |
# Format large numbers with commas | |
formatted_value = f"{value:,}" | |
elif key == 'duration': | |
# Convert seconds to HH:MM:SS format | |
hours, remainder = divmod(value, 3600) | |
minutes, seconds = divmod(remainder, 60) | |
formatted_value = f"{hours:02d}:{minutes:02d}:{seconds:02d}" | |
else: | |
formatted_value = str(value) | |
formatted_text += f"{key.capitalize()}: {formatted_value}\n" | |
return formatted_text.strip() | |
# # Example usage: | |
# example_metadata = { | |
# 'title': 'Sample Video Title', | |
# 'uploader': 'Channel Name', | |
# 'upload_date': '20230615', | |
# 'view_count': 1000000, | |
# 'like_count': 50000, | |
# 'duration': 3725, # 1 hour, 2 minutes, 5 seconds | |
# 'tags': ['tag1', 'tag2', 'tag3'], | |
# 'description': 'This is a sample video description.' | |
# } | |
# | |
# print(format_metadata_as_text(example_metadata)) | |
def convert_to_seconds(time_str): | |
if not time_str: | |
return 0 | |
# If it's already a number, assume it's in seconds | |
if time_str.isdigit(): | |
return int(time_str) | |
# Parse time string in format HH:MM:SS, MM:SS, or SS | |
time_parts = time_str.split(':') | |
if len(time_parts) == 3: | |
return int(timedelta(hours=int(time_parts[0]), | |
minutes=int(time_parts[1]), | |
seconds=int(time_parts[2])).total_seconds()) | |
elif len(time_parts) == 2: | |
return int(timedelta(minutes=int(time_parts[0]), | |
seconds=int(time_parts[1])).total_seconds()) | |
elif len(time_parts) == 1: | |
return int(time_parts[0]) | |
else: | |
raise ValueError(f"Invalid time format: {time_str}") | |
# | |
# End of Misc-Functions | |
####################################################################################################################### | |
####################################################################################################################### | |
# | |
# File-saving Function Definitions | |
def save_to_file(video_urls, filename): | |
with open(filename, 'w') as file: | |
file.write('\n'.join(video_urls)) | |
print(f"Video URLs saved to {filename}") | |
def save_segments_to_json(segments, file_name="transcription_segments.json"): | |
""" | |
Save transcription segments to a JSON file. | |
Parameters: | |
segments (list): List of transcription segments | |
file_name (str): Name of the JSON file to save (default: "transcription_segments.json") | |
Returns: | |
str: Path to the saved JSON file | |
""" | |
# Ensure the Results directory exists | |
os.makedirs("Results", exist_ok=True) | |
# Full path for the JSON file | |
json_file_path = os.path.join("Results", file_name) | |
# Save segments to JSON file | |
with open(json_file_path, 'w', encoding='utf-8') as json_file: | |
json.dump(segments, json_file, ensure_ascii=False, indent=4) | |
return json_file_path | |
def download_file(url, dest_path, expected_checksum=None, max_retries=3, delay=5): | |
temp_path = dest_path + '.tmp' | |
for attempt in range(max_retries): | |
try: | |
# Check if a partial download exists and get its size | |
resume_header = {} | |
if os.path.exists(temp_path): | |
resume_header = {'Range': f'bytes={os.path.getsize(temp_path)}-'} | |
response = requests.get(url, stream=True, headers=resume_header) | |
response.raise_for_status() | |
# Get the total file size from headers | |
total_size = int(response.headers.get('content-length', 0)) | |
initial_pos = os.path.getsize(temp_path) if os.path.exists(temp_path) else 0 | |
mode = 'ab' if 'Range' in response.headers else 'wb' | |
with open(temp_path, mode) as temp_file, tqdm( | |
total=total_size, unit='B', unit_scale=True, desc=dest_path, initial=initial_pos, ascii=True | |
) as pbar: | |
for chunk in response.iter_content(chunk_size=8192): | |
if chunk: # filter out keep-alive new chunks | |
temp_file.write(chunk) | |
pbar.update(len(chunk)) | |
# Verify the checksum if provided | |
if expected_checksum: | |
if not verify_checksum(temp_path, expected_checksum): | |
os.remove(temp_path) | |
raise ValueError("Downloaded file's checksum does not match the expected checksum") | |
# Move the file to the final destination | |
os.rename(temp_path, dest_path) | |
print("Download complete and verified!") | |
return dest_path | |
except Exception as e: | |
print(f"Attempt {attempt + 1} failed: {e}") | |
if attempt < max_retries - 1: | |
print(f"Retrying in {delay} seconds...") | |
time.sleep(delay) | |
else: | |
print("Max retries reached. Download failed.") | |
raise | |
def create_download_directory(title): | |
base_dir = "Results" | |
# Remove characters that are illegal in Windows filenames and normalize | |
safe_title = normalize_title(title) | |
logging.debug(f"{title} successfully normalized") | |
session_path = os.path.join(base_dir, safe_title) | |
if not os.path.exists(session_path): | |
os.makedirs(session_path, exist_ok=True) | |
logging.debug(f"Created directory for downloaded video: {session_path}") | |
else: | |
logging.debug(f"Directory already exists for downloaded video: {session_path}") | |
return session_path | |
def safe_read_file(file_path): | |
encodings = ['utf-8', 'utf-16', 'ascii', 'latin-1', 'iso-8859-1', 'cp1252'] | |
for encoding in encodings: | |
try: | |
with open(file_path, 'r', encoding=encoding) as file: | |
return file.read() | |
except UnicodeDecodeError: | |
continue | |
except FileNotFoundError: | |
return f"File not found: {file_path}" | |
except Exception as e: | |
return f"An error occurred: {e}" | |
return f"Unable to decode the file {file_path} with any of the attempted encodings: {encodings}" | |
# | |
# End of Files-saving Function Definitions | |
####################################################################################################################### | |
####################################################################################################################### | |
# | |
# UUID-Functions | |
def generate_unique_filename(base_path, base_filename): | |
"""Generate a unique filename by appending a counter if necessary.""" | |
filename = base_filename | |
counter = 1 | |
while os.path.exists(os.path.join(base_path, filename)): | |
name, ext = os.path.splitext(base_filename) | |
filename = f"{name}_{counter}{ext}" | |
counter += 1 | |
return filename | |
def generate_unique_identifier(file_path): | |
filename = os.path.basename(file_path) | |
timestamp = int(time.time()) | |
# Generate a hash of the file content | |
hasher = hashlib.md5() | |
with open(file_path, 'rb') as f: | |
buf = f.read() | |
hasher.update(buf) | |
content_hash = hasher.hexdigest()[:8] # Use first 8 characters of the hash | |
return f"local:{timestamp}:{content_hash}:{filename}" | |
# | |
# End of UUID-Functions | |
####################################################################################################################### | |
####################################################################################################################### | |
# | |
# Backup code | |
# | |
# End of backup code | |
####################################################################################################################### | |
####################################################################################################################### | |
# | |
# Sanitization/Verification Functions | |
# Helper function to validate URL format | |
def is_valid_url(url: str) -> bool: | |
regex = re.compile( | |
r'^(?:http|ftp)s?://' # http:// or https:// | |
r'(?:(?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\.)+(?:[A-Z]{2,6}\.?|[A-Z0-9-]{2,}\.?)|' # domain... | |
r'localhost|' # localhost... | |
r'\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}|' # ...or ipv4 | |
r'\[?[A-F0-9]*:[A-F0-9:]+\]?)' # ...or ipv6 | |
r'(?::\d+)?' # optional port | |
r'(?:/?|[/?]\S+)$', re.IGNORECASE) | |
return re.match(regex, url) is not None | |
def verify_checksum(file_path, expected_checksum): | |
sha256_hash = hashlib.sha256() | |
with open(file_path, 'rb') as f: | |
for byte_block in iter(lambda: f.read(4096), b''): | |
sha256_hash.update(byte_block) | |
return sha256_hash.hexdigest() == expected_checksum | |
def normalize_title(title): | |
# Normalize the string to 'NFKD' form and encode to 'ascii' ignoring non-ascii characters | |
title = unicodedata.normalize('NFKD', title).encode('ascii', 'ignore').decode('ascii') | |
title = title.replace('/', '_').replace('\\', '_').replace(':', '_').replace('"', '').replace('*', '').replace('?', | |
'').replace( | |
'<', '').replace('>', '').replace('|', '') | |
return title | |
def clean_youtube_url(url): | |
parsed_url = urlparse(url) | |
query_params = parse_qs(parsed_url.query) | |
if 'list' in query_params: | |
query_params.pop('list') | |
cleaned_query = urlencode(query_params, doseq=True) | |
cleaned_url = urlunparse(parsed_url._replace(query=cleaned_query)) | |
return cleaned_url | |
def sanitize_filename(filename): | |
# Remove invalid characters and replace spaces with underscores | |
sanitized = re.sub(r'[<>:"/\\|?*]', '', filename) | |
sanitized = re.sub(r'\s+', ' ', sanitized).strip() | |
return sanitized | |
def format_transcription(content): | |
# Replace '\n' with actual line breaks | |
content = content.replace('\\n', '\n') | |
# Split the content by newlines first | |
lines = content.split('\n') | |
formatted_lines = [] | |
for line in lines: | |
# Add extra space after periods for better readability | |
line = line.replace('.', '. ').replace('. ', '. ') | |
# Split into sentences using a more comprehensive regex | |
sentences = re.split('(?<=[.!?]) +', line) | |
# Trim whitespace from each sentence and add a line break | |
formatted_sentences = [sentence.strip() for sentence in sentences if sentence.strip()] | |
# Join the formatted sentences | |
formatted_lines.append(' '.join(formatted_sentences)) | |
# Join the lines with HTML line breaks | |
formatted_content = '<br>'.join(formatted_lines) | |
return formatted_content | |
def format_file_path(file_path, fallback_path=None): | |
if file_path and os.path.exists(file_path): | |
logging.debug(f"File exists: {file_path}") | |
return file_path | |
elif fallback_path and os.path.exists(fallback_path): | |
logging.debug(f"File does not exist: {file_path}. Returning fallback path: {fallback_path}") | |
return fallback_path | |
else: | |
logging.debug(f"File does not exist: {file_path}. No fallback path available.") | |
return None | |
# | |
# End of Sanitization/Verification Functions | |
####################################################################################################################### | |
####################################################################################################################### | |
# | |
# DB Config Loading | |
def get_db_config(): | |
config = configparser.ConfigParser() | |
config.read('config.txt') | |
return { | |
'type': config['Database']['type'], | |
'sqlite_path': config.get('Database', 'sqlite_path', fallback='media_summary.db'), | |
'elasticsearch_host': config.get('Database', 'elasticsearch_host', fallback='localhost'), | |
'elasticsearch_port': config.getint('Database', 'elasticsearch_port', fallback=9200) | |
} | |
# | |
# End of DB Config Loading | |
####################################################################################################################### | |