Spaces:
Sleeping
Sleeping
| import os | |
| import tempfile | |
| import networkx as nx | |
| import sympy as sp | |
| import re | |
| from collections import defaultdict | |
| import gradio as gr | |
| from gradio.themes import Ocean | |
| from huggingface_hub import InferenceClient | |
| import requests | |
| from PIL import Image | |
| from io import BytesIO | |
| # --- Set your HF token --- | |
| # Either hardcode here or use Colab's userdata like: | |
| HF_TOKEN = os.environ.get("HF_TOKEN") | |
| if not HF_TOKEN: | |
| raise RuntimeError("HF_TOKEN environment variable not set. Please add it in your Space's secrets.") | |
| from huggingface_hub import InferenceClient | |
| client = InferenceClient( | |
| provider="featherless-ai", | |
| api_key=os.environ["HF_TOKEN"], | |
| ) | |
| # --- Helper: Save PIL image to URL-accessible temp file --- | |
| def image_to_temp_url(image): | |
| temp_path = tempfile.NamedTemporaryFile(delete=False, suffix=".png") | |
| image.save(temp_path.name) | |
| return "https://your-server.com/temporary-image-support.png" # placeholder (host image externally if needed) | |
| # --- OR upload image to Hugging Face Space / GDrive and return a public URL instead | |
| # You can use this for production use | |
| def extract_network_from_image(image): | |
| # Upload image to temp path | |
| image_path = tempfile.NamedTemporaryFile(suffix=".png", delete=False).name | |
| image.save(image_path) | |
| # Upload manually or serve image online if needed | |
| # For now, simulate by loading image into bytes and re-uploading to HF or GDrive | |
| # Instead: In Colab, just use direct GDrive URLs | |
| # Placeholder: Manually put a URL here for now (from GDrive or HF Spaces or web) | |
| raise NotImplementedError("Replace this with your public image URL logic.") | |
| # New: Directly send the URL to Unsloth Mistral + get output | |
| def extract_network_from_url(image_url): | |
| prompt = ( | |
| "Analyze this network diagram and list the network only, e.g. Q + W -> R. Do not print any other sentence except the network." | |
| "The arrows represent reactions. If there are multiple reactions, give them comma separated like A -> B, B -> C, etc." | |
| ) | |
| completion = client.chat.completions.create( | |
| model="unsloth/Mistral-Small-3.2-24B-Instruct-2506", | |
| messages=[ | |
| { | |
| "role": "user", | |
| "content": [ | |
| {"type": "text", "text": prompt}, | |
| {"type": "image_url", "image_url": {"url": image_url}}, | |
| ] | |
| } | |
| ] | |
| ) | |
| return completion.choices[0].message.content.strip() | |
| # --- Network Analysis Functions --- | |
| def parse_species(expr): | |
| return [s.strip() for s in re.split(r'\s*[\+\-]\s*', expr)] | |
| def parse_network(input_string): | |
| edges, reversible_edges = [], [] | |
| for part in input_string.split(','): | |
| part = part.strip() | |
| if '<->' in part: | |
| lhs, rhs = part.split('<->') | |
| lhs_species = parse_species(lhs) | |
| rhs_species = parse_species(rhs) | |
| reversible_edges.append((lhs_species, rhs_species)) | |
| elif '->' in part: | |
| lhs, rhs = part.split('->') | |
| lhs_species = parse_species(lhs) | |
| rhs_species = parse_species(rhs) | |
| edges.append((lhs_species, rhs_species)) | |
| return edges, reversible_edges | |
| def build_graph(edges, reversible_edges): | |
| G = nx.DiGraph() | |
| for a, b in edges: | |
| lhs = " + ".join(a) | |
| rhs = " + ".join(b) | |
| G.add_edge(lhs, rhs) | |
| for a, b in reversible_edges: | |
| lhs = " + ".join(a) | |
| rhs = " + ".join(b) | |
| G.add_edge(lhs, rhs) | |
| G.add_edge(rhs, lhs) | |
| return G | |
| def analyze_graph(G): | |
| return { | |
| "nodes": list(G.nodes), | |
| "edges": list(G.edges), | |
| "num_nodes": G.number_of_nodes(), | |
| "num_edges": G.number_of_edges(), | |
| "is_cyclic": not nx.is_directed_acyclic_graph(G) | |
| } | |
| def mass_action_odes(edges, reversible_edges): | |
| species = set() | |
| odes = defaultdict(lambda: 0) | |
| rate_counter = 1 | |
| def term(species_list): | |
| term_expr = 1 | |
| for s in species_list: | |
| sym = sp.symbols(s) | |
| species.add(sym) | |
| term_expr *= sym | |
| return term_expr | |
| for lhs_species, rhs_species in edges: | |
| k = sp.symbols(f'k{rate_counter}') | |
| rate_counter += 1 | |
| flux = k * term(lhs_species) | |
| for s in lhs_species: | |
| sym = sp.symbols(s) | |
| odes[sym] -= flux | |
| for s in rhs_species: | |
| sym = sp.symbols(s) | |
| odes[sym] += flux | |
| for lhs_species, rhs_species in reversible_edges: | |
| kf = sp.symbols(f'k{rate_counter}') | |
| rate_counter += 1 | |
| kr = sp.symbols(f'k{rate_counter}') | |
| rate_counter += 1 | |
| forward_flux = kf * term(lhs_species) | |
| reverse_flux = kr * term(rhs_species) | |
| for s in lhs_species: | |
| sym = sp.symbols(s) | |
| odes[sym] -= forward_flux | |
| odes[sym] += reverse_flux | |
| for s in rhs_species: | |
| sym = sp.symbols(s) | |
| odes[sym] += forward_flux | |
| odes[sym] -= reverse_flux | |
| return dict(odes) | |
| def format_odes(odes): | |
| return "\n".join([f"d{var}/dt = {sp.simplify(expr)}" for var, expr in odes.items()]) | |
| def compute_jacobian(odes): | |
| variables = list(odes.keys()) | |
| F = sp.Matrix([odes[var] for var in variables]) | |
| J = F.jacobian(variables) | |
| return sp.pretty(J) | |
| def process_network(input_string, query, image_url=None): | |
| edges, reversible_edges = parse_network(input_string) | |
| G = build_graph(edges, reversible_edges) | |
| info = analyze_graph(G) | |
| if 'ode' in query.lower(): | |
| ode_sys = mass_action_odes(edges, reversible_edges) | |
| return format_odes(ode_sys) | |
| elif 'jacobian' in query.lower(): | |
| ode_sys = mass_action_odes(edges, reversible_edges) | |
| return f"Jacobian Matrix:\n{compute_jacobian(ode_sys)}" | |
| elif 'variables' in query.lower(): | |
| return f"There are {info['num_nodes']} variables: {info['nodes']}" | |
| elif 'edges' in query.lower(): | |
| return f"Edges: {info['edges']}" | |
| elif 'cyclic' in query.lower() or 'cycle' in query.lower(): | |
| cycles = list(nx.simple_cycles(G)) | |
| return "Cycles found:\n" + "\n".join([" -> ".join(cycle + [cycle[0]]) for cycle in cycles]) if cycles else "No cycles found." | |
| # Fallback: Use LLM on both image and parsed network | |
| else: | |
| content = [ | |
| { | |
| "type": "text", | |
| "text": ( | |
| "You are given a biological network with the following structure:\n" | |
| f"β’ Nodes: {info['nodes']}\n" | |
| f"β’ Reactions (edges): {info['edges']}\n\n" | |
| f"Answer the following query based on this structure and the image:" | |
| f"\n\n{query}" | |
| ), | |
| } | |
| ] | |
| if image_url: | |
| content.append({ | |
| "type": "image_url", | |
| "image_url": {"url": image_url} | |
| }) | |
| response = client.chat.completions.create( | |
| model="unsloth/Mistral-Small-3.2-24B-Instruct-2506", | |
| messages=[{"role": "user", "content": content}], | |
| ) | |
| return response.choices[0].message.content.strip() | |
| # --- Full Gradio Handler --- | |
| def full_process(text_input, image_url, query): | |
| image_preview = None | |
| network_description = "" | |
| result = "" | |
| if text_input.strip(): | |
| network_description = text_input.strip() | |
| elif image_url.strip(): | |
| # Display image from URL | |
| try: | |
| response = requests.get(image_url) | |
| image_preview = Image.open(BytesIO(response.content)) | |
| except: | |
| return None, "", "β Invalid image URL" | |
| # Extract network | |
| network_description = extract_network_from_url(image_url) | |
| else: | |
| return None, "", "β Provide text or image URL." | |
| # Answer query | |
| result = process_network(network_description, query, image_url=image_url if image_url.strip() else None) | |
| return image_preview, network_description, result | |
| import gradio as gr | |
| from gradio.themes.utils import sizes | |
| from gradio.themes.base import Base | |
| from gradio.themes.utils import colors | |
| # Optional: Keep your theme | |
| theme = gr.themes.Ocean() | |
| with gr.Blocks(theme=theme, css="#footer-link {text-align: center; font-size: 14px; color: #555;}") as iface: | |
| gr.Markdown("## π¬ Biological Network Analyzer") | |
| gr.Markdown("Paste a network OR provide a public image URL. Then ask a query like **'Give ODEs'** or **'Is it cyclic?'**") | |
| with gr.Row(): | |
| with gr.Column(): | |
| # img_input = gr.Image(type="pil", label="Upload Network Image (β Not supported unless image is hosted online)") | |
| text_input = gr.Textbox(label="Text Input (optional)", placeholder="Or paste network: A + B -> C, X <-> Y") | |
| url_input = gr.Textbox(label="π Public Image URL (e.g., from GDrive)", placeholder="https://... (must be accessible)") | |
| query_input = gr.Textbox(label="Query", placeholder="Ask about ODEs, Jacobian, edges, etc.") | |
| with gr.Column(): | |
| img_output = gr.Image(label="πΌοΈ Image Preview") | |
| network_text = gr.Textbox(label="π§ͺ Extracted Network") | |
| result_box = gr.Textbox(label="π Answer") | |
| # Link logic to function | |
| inputs = [text_input, url_input, query_input] | |
| outputs = [img_output, network_text, result_box] | |
| iface_fn = gr.Interface(fn=full_process, inputs=inputs, outputs=outputs) | |
| # Footer GitHub link | |
| gr.Markdown(""" | |
| <footer style='text-align:center; margin-top:20px; color:#aaa;'> | |
| Built using Gradio, Hugging Face & Mistral | | |
| <a href="https://github.com/kumardevansh/network_analyzer" target="_blank" style="color:#aaa; text-decoration:underline;"> | |
| View on GitHub | |
| </a> | |
| </footer> | |
| """) | |
| iface.launch(share=True) | |