"""Streamlit app for video transcription using Snowflake Cortex.""" from functools import cache import streamlit as st import tempfile import os import json import pandas as pd import io # New import for file streaming import ffmpeg import snowflake.snowpark as snowpark from snowflake.core import Root # # Import our Snowflake utilities from snowflake_utils import init_snowflake_session, test_snowflake_connection # We can also use Snowpark for our analyses! # from snowflake.snowpark.context import get_active_session # session = get_active_session() # Snowflake connection configuration def init_snowflake_connection(): """Initialize Snowflake connection with multiple credential sources.""" # Fall back to our custom connection method try: session = init_snowflake_session() return session except Exception as e: print(f"Failed to initialize Snowflake connection: {str(e)}") return None # NOTE: The upload function must be completely rewritten to use Snowpark's put_stream # and must NOT contain the PUT SQL command. session = init_snowflake_connection() @st.cache_data def get_summary(text_blob): """Get summary.""" summary = session.sql(""" SELECT SNOWFLAKE.CORTEX.AI_COMPLETE( 'llama3-8b', CONCAT( 'You are a summary generator for video alt-text. Analyze the following text and generate a short yet precise summary as a string with max 300 characters. NO DOT include any data in the text output that is not the summary. Only the summary text. Text: ', ? -- Placeholder for the text_blob input ) ) AS VIDEO_SUMMARY; """, params=[text_blob]).collect() return summary @st.cache_data def get_keywords(text_blob): """Lookup keywords.""" keywords_result = session.sql(""" SELECT TRIM(f.value::STRING, '"') AS keyword FROM TABLE( FLATTEN( INPUT => PARSE_JSON( SNOWFLAKE.CORTEX.AI_COMPLETE( 'llama3-8b', CONCAT('You are a precise keyword extractor. Analyze the following text and return a JSON array of 15 most relevant single-word or short-phrase keywords. ONLY return the JSON array, no other text. Text: ', ?) ) ) ) ) AS f """, params=[text_blob]).collect() return keywords_result @st.cache_data def get_final_hashtag_matches(summary, keywords, hashtags): """ Use keywords and summary to filter down hashtags to top 5 matches. Args: summary (str): Video summary text keywords (list): List of extracted keywords hashtags (list): List of hashtag dictionaries from vector search Returns: list: Top 5 hashtag matches as list of dictionaries """ if not hashtags or len(hashtags) == 0: return [] try: # Convert inputs to strings for processing keywords_str = ", ".join(keywords) if isinstance(keywords, list) else str(keywords) hashtags_json = json.dumps(hashtags) # Use Snowflake Cortex to analyze and rank hashtags analysis_result = session.sql(""" SELECT SNOWFLAKE.CORTEX.AI_COMPLETE( 'llama3-8b', CONCAT( 'You are a hashtag ranking expert. Given keywords, summary, and hashtags data, return the top 5 most relevant hashtags as a JSON array.', ' Each item should be the original hashtag object from the input.', ' Rank by relevance to keywords and summary, considering engagement metrics (views, likes).', ' Keywords: ', ?, ' Summary: ', ?, ' Hashtags: ', ?, ' Return ONLY the JSON array of top 5 hashtag objects, no other text.' ) ) as ranked_hashtags """, params=[keywords_str, summary, hashtags_json]).collect() if analysis_result and len(analysis_result) > 0: result_text = analysis_result[0]['RANKED_HASHTAGS'] try: # Parse the JSON response top_hashtags = json.loads(result_text) if isinstance(top_hashtags, list): return top_hashtags[:5] # Ensure max 5 results else: print(f"Warning: AI returned non-list result: {result_text}") return fallback_ranking(summary, keywords, hashtags) except json.JSONDecodeError as e: print(f"Warning: Failed to parse AI response as JSON: {e}") return fallback_ranking(summary, keywords, hashtags) else: return fallback_ranking(summary, keywords, hashtags) except Exception as e: print(f"Error in get_final_hashtag_matches: {e}") return fallback_ranking(summary, keywords, hashtags) def fallback_ranking(summary, keywords, hashtags): """ Fallback algorithm for ranking hashtags when AI fails. Args: summary (str): Video summary text keywords (list): List of extracted keywords hashtags (list): List of hashtag dictionaries Returns: list: Top 5 ranked hashtags """ try: scored_hashtags = [] summary_lower = summary.lower() if summary else "" keywords_lower = [k.lower() for k in keywords] if keywords else [] for hashtag_data in hashtags: if not isinstance(hashtag_data, dict): continue hashtag = hashtag_data.get('hashtag', '').lower() total_views = hashtag_data.get('total_views', 0) total_likes = hashtag_data.get('total_likes', 0) # Calculate relevance score score = 0 # Keyword matching (40% weight) for keyword in keywords_lower: if keyword in hashtag: score += 40 elif any(word in hashtag for word in keyword.split()): score += 20 # Summary matching (30% weight) if hashtag in summary_lower: score += 30 elif any(word in summary_lower for word in hashtag.split()): score += 15 # Engagement metrics (30% weight) # Normalize views and likes to 0-30 scale max_views = max([h.get('total_views', 0) for h in hashtags] + [1]) max_likes = max([h.get('total_likes', 0) for h in hashtags] + [1]) views_score = (total_views / max_views) * 15 if max_views > 0 else 0 likes_score = (total_likes / max_likes) * 15 if max_likes > 0 else 0 score += views_score + likes_score scored_hashtags.append({ **hashtag_data, 'relevance_score': score }) # Sort by score and return top 5 scored_hashtags.sort(key=lambda x: x.get('relevance_score', 0), reverse=True) # Remove the temporary score field from results final_results = [] for item in scored_hashtags[:5]: result_item = {k: v for k, v in item.items() if k != 'relevance_score'} final_results.append(result_item) return final_results except Exception as e: print(f"Error in fallback_ranking: {e}") return hashtags[:5] if hashtags else [] def get_search_data(table_name, column_name, keywords, summary): """Get search data.""" # First, ensure we're using the correct warehouse and database print("๐ญ Setting active warehouse and database...") try: session.sql("USE WAREHOUSE TEAM_04_WH").collect() session.sql("USE DATABASE TEAM_04_DB").collect() session.sql("USE SCHEMA MYSCHEMA").collect() print("โ Successfully set context to TEAM_04_DB.MYSCHEMA") except Exception as context_error: print(f"โ ๏ธ Could not set full context: {context_error}") print("๐ Trying alternative approach...") # Use existing Cortex Search Service with proper API print("๐ Using existing Cortex Search Service: transcript_search_service") # Use the Cortex Search Service to find relevant hashtags print("๐ Searching for relevant hashtags using semantic search...") try: # Combine all keywords into a search query search_text = " ".join(keywords) # Use the snowflake.core API to access the search service root = Root(session) my_service = (root .databases["TEAM_04_DB"] .schemas["MYSCHEMA"] .cortex_search_services["transcript_search_service_v2_trend"] ) # Query the service with proper parameters to include additional columns search_response = my_service.search( query=search_text, columns=["distint_hashtag", "country_code", "total_views", "total_likes"], filters={ "@or": [ { "@eq": { "country_code": "US" } }, { "@contains": { "country_code": "CA" } } ] }, limit=20 ) # Convert response to list of dictionaries including all requested columns data = [] seen_hashtags = set() # Track seen hashtags to avoid duplicates for result in search_response.results: hashtag = result.get('distint_hashtag', '') # Only add if we haven't seen this hashtag before if hashtag and hashtag not in seen_hashtags: seen_hashtags.add(hashtag) data.append({ 'hashtag': hashtag, 'country_code': result.get('country_code', ''), 'total_views': result.get('total_views', 0), 'total_likes': result.get('total_likes', 0) }) if data: print(f"โ Found {len(data)} unique hashtags after deduplication!") return data else: print("โ ๏ธ No similar hashtags found") return [{"status": "No similar hashtags found using existing search service"}] except Exception as search_error: print(f"โ Error using existing Cortex Search Service: {search_error}") print("๐ Falling back to simple keyword matching...") return simple_keyword_search(keywords, table_name, column_name) def lookup_keywords_in_snowflake(text_blob): """ Uses a Snowflake connection to execute a Cortex-powered SQL query. Args: text_blob (str): The input text to extract keywords from. table_name (str): The name of the table to search (e.g., 'EXAMPLE_TABLE'). connection_details (dict): Dictionary containing Snowflake connection parameters. """ # The SQL query uses Snowflake Cortex (AI_COMPLETE) to get keywords # and then uses those keywords to filter the target table. table_name = "COLLABJAM_TIKTOK_DISTINCTHASHTAG" column_name = "DISTINCT_HASHTAG" try: # Step 1: Execute keyword extraction query using parameter binding print("๐ง Extracting keywords using AI...") # Use Snowpark's parameter binding to safely pass the text keywords_result = get_keywords(text_blob) summary_result = get_summary(text_blob) summary = summary_result[0]["VIDEO_SUMMARY"] # Extract keywords into a Python list keywords = [row['KEYWORD'] for row in keywords_result if row['KEYWORD']] if not keywords: print("โ ๏ธ No keywords extracted from the text") return None # Display all extracted keywords clearly st.success(f"โ Extracted {len(keywords)} keywords:") st.write("**Keywords found:**") for i, keyword in enumerate(keywords, 1): st.write(f"{i}. `{keyword}`") # Step 2: Create Cortex Search Service instead of traditional search # Create ILIKE conditions for each keyword using only HASHTAGS column data = get_search_data(table_name=table_name, column_name=column_name, keywords=keywords, summary=summary_result) final_result = get_final_hashtag_matches(keywords=keywords, summary=summary_result, hashtags=data) return (final_result, summary) except Exception as e: print(f"An error occurred during keyword lookup: {e}") print(f"An error occurred: {e}") return None def simple_keyword_search(keywords, table_name, column_name): """Fallback function for simple keyword matching when Cortex Search Service fails.""" try: # Create parameterized conditions for safe SQL execution conditions = [] params_list = [] for keyword in keywords: # Use parameter binding for each keyword search conditions.append(f"(t.{column_name} ILIKE ?)") keyword_pattern = f"%{keyword}%" params_list.append(keyword_pattern) if not conditions: st.warning("โ ๏ธ No valid search conditions generated") return None # Combine conditions with OR where_clause = " OR ".join(conditions) search_query = f""" SELECT t.* FROM {table_name} AS t WHERE {where_clause} LIMIT 20 """ print("๐ Searching with simple keyword matching...") result = session.sql(search_query, params=params_list).collect() # Convert Snowpark Row objects to list of dictionaries data = [] for row in result: data.append(row.as_dict()) return data except Exception as e: print(f"โ Simple keyword search also failed: {str(e)}") return None except Exception as e: print(f"An error occurred during keyword lookup: {e}") print(f"An error occurred: {e}") return None def convert_mp4_to_flac(input_path): """Convert MP4 video to FLAC audio using FFmpeg - fast version with audio stream copy.""" try: # Create temporary file for output with tempfile.NamedTemporaryFile(delete=False, suffix=".flac") as temp_flac: output_path = temp_flac.name # Fast conversion: copy audio stream without re-encoding when possible # -vn: no video (discard video stream) # -c:a copy: copy audio codec without re-encoding (fastest) # Falls back to flac encoding if copy fails try: # First try: direct audio copy (fastest) ( ffmpeg .input(input_path) .output(output_path, vn=None, **{'c:a': 'copy'}) .overwrite_output() .run(quiet=True, capture_stdout=True) ) except Exception as e: # Fallback: re-encode to FLAC if copy fails (some formats need conversion) print(e) ( ffmpeg .input(input_path) .output(output_path, vn=None, acodec='flac', ar=44100, ac=2) .overwrite_output() .run(quiet=True, capture_stdout=True) ) return output_path # except ffmpeg.Error as e: # print(f"โ FFmpeg conversion failed: {e.stderr.decode() if e.stderr else str(e)}") # return None except Exception as e: print(f"โ Audio conversion error: {str(e)}") return None def upload_audio_to_stage(file_path, stage_name="audio_data_upload"): """Upload FLAC audio file to Snowflake stage.""" # Check if session is valid if session is None: print("โ Invalid Snowflake session - cannot upload audio file") return None # Create audio stage if it doesn't exist - try different approaches try: # First, try to create in current schema create_stage_sql = f""" CREATE STAGE IF NOT EXISTS {stage_name} ENCRYPTION = (TYPE = 'SNOWFLAKE_SSE'); """ session.sql(create_stage_sql).collect() print(f"โ Stage '{stage_name}' ready in current schema") except Exception as e1: # If current schema fails, try PUBLIC schema try: public_stage_name = f"PUBLIC.{stage_name}" create_stage_sql = f""" CREATE STAGE IF NOT EXISTS {public_stage_name} ENCRYPTION = (TYPE = 'SNOWFLAKE_SSE'); """ session.sql(create_stage_sql).collect() stage_name = public_stage_name # Update stage name for later use print(f"โ Stage created in PUBLIC schema: {public_stage_name}") except Exception as e2: # If both fail, try without CREATE (stage might already exist) try: # Test if stage exists by listing it test_sql = f"LIST @{stage_name}" session.sql(test_sql).collect() print(f"โ Using existing stage: {stage_name}") except Exception as e3: print(f"โ Cannot create or access stage. Tried multiple approaches:") print(f" Current schema: {str(e1)}") print(f" PUBLIC schema: {str(e2)}") print(f" Existing stage: {str(e3)}") print("๐ก Suggestion: Ask your admin to grant CREATE STAGE privileges or create the stage manually") return None # Upload FLAC file filename = os.path.basename(file_path) stage_path = f"@{stage_name}/{filename}" try: with open(file_path, 'rb') as f: file_stream = io.BytesIO(f.read()) session.file.put_stream( file_stream, stage_path, auto_compress=False, overwrite=True ) return filename except Exception as e: print(f"โ Audio upload to stage failed: {str(e)}") return None def transcribe_video_with_cortex(filename, stage_name="vid_data_upload"): """Transcribe video file using Snowflake Cortex AI.""" try: # Use session.sql() and collect() for SQL queries transcribe_sql = f""" SELECT AI_TRANSCRIBE( TO_FILE('@{stage_name}', '{filename}'), {{'timestamp_granularity': 'word'}} ) as transcription """ result = session.sql(transcribe_sql).collect() if result and len(result) > 0 and result[0]['TRANSCRIPTION']: # The result from Snowpark collect is a list of Row objects # Access the column value using the column name return json.loads(result[0]['TRANSCRIPTION']) else: return None except Exception as e: print(f"file: {filename}") print(f"Error during transcription: {str(e)}") return None def transcribe_audio_with_cortex(filename, stage_name="audio_data_upload"): """Transcribe audio file using Snowflake Cortex AI.""" # Check if session is valid if session is None: print("โ Invalid Snowflake session - cannot transcribe audio") return None try: # Use session.sql() and collect() for SQL queries transcribe_sql = f""" SELECT AI_TRANSCRIBE( TO_FILE('@{stage_name}', '{filename}'), {{'timestamp_granularity': 'word'}} ) as transcription """ result = session.sql(transcribe_sql).collect() if result and len(result) > 0 and result[0]['TRANSCRIPTION']: return json.loads(result[0]['TRANSCRIPTION']) else: return None except Exception as e: print(f"Audio file: {filename}") print(f"Error during audio transcription: {str(e)}") return None def main(): """Main Streamlit application.""" # --- 1. Basic Setup and Page Config --- st.set_page_config( page_title="TRENDscribe", layout="centered", initial_sidebar_state="collapsed" ) # Display the TRENDscribe logo as the title col1, col2, col3 = st.columns([0.05, 50, 0.05]) with col2: try: st.image("title_image.png", use_container_width=True) except: # Fallback to text title if image not found st.title("๐ฌ TRENDscribe!") st.markdown("