| import gradio as gr
|
| import pandas as pd
|
| import numpy as np
|
| import os
|
| from datetime import datetime
|
| import tempfile
|
| from collections import defaultdict
|
|
|
|
|
| REQUIRED_COLS = [
|
| "Account",
|
| "Order #",
|
| "DESIGN",
|
| "Labels",
|
| "Colours",
|
| "Kgs",
|
| "Pending"
|
| ]
|
|
|
|
|
| OPTIONAL_COLS = ["Sqm", "Unnamed: 0"]
|
|
|
| def _normalize_columns(df: pd.DataFrame) -> pd.DataFrame:
|
| """Normalize column names by stripping whitespace"""
|
| df = df.copy()
|
| df.columns = [str(c).strip() for c in df.columns]
|
| return df
|
|
|
| def _parse_colours(colour_str):
|
| """Parse colour string into list of individual colours"""
|
| if pd.isna(colour_str):
|
| return []
|
|
|
|
|
| colour_str = str(colour_str).strip()
|
|
|
|
|
| for sep in [',', ';', '|', '/', '+', '&']:
|
| if sep in colour_str:
|
| colours = [c.strip().upper() for c in colour_str.split(sep) if c.strip()]
|
| return colours
|
|
|
|
|
| return [colour_str.upper()] if colour_str else []
|
|
|
| def calculate_colour_totals(df: pd.DataFrame) -> pd.DataFrame:
|
| """Calculate total quantity required for each colour across all designs"""
|
| colour_totals = defaultdict(float)
|
| colour_details = defaultdict(list)
|
|
|
| for _, row in df.iterrows():
|
| colours = _parse_colours(row['Colours'])
|
| kgs = pd.to_numeric(row['Kgs'], errors='coerce') or 0
|
| design = str(row.get('DESIGN', 'Unknown'))
|
| order_num = str(row.get('Order #', 'Unknown'))
|
|
|
| if colours and kgs > 0:
|
|
|
| kgs_per_colour = kgs / len(colours)
|
| for colour in colours:
|
| colour_totals[colour] += kgs_per_colour
|
| colour_details[colour].append({
|
| 'Design': design,
|
| 'Order': order_num,
|
| 'Kgs_Contribution': kgs_per_colour,
|
| 'Total_Order_Kgs': kgs
|
| })
|
|
|
|
|
| colour_rows = []
|
| for colour, total_kgs in sorted(colour_totals.items(), key=lambda x: x[1], reverse=True):
|
| designs_using = list(set([detail['Design'] for detail in colour_details[colour]]))
|
| orders_count = len(colour_details[colour])
|
|
|
| colour_rows.append({
|
| 'Colour': colour,
|
| 'Total_Kgs_Required': round(total_kgs, 2),
|
| 'Designs_Using_This_Colour': ', '.join(sorted(designs_using)),
|
| 'Number_of_Orders': orders_count,
|
| 'Priority_Rank': len(colour_rows) + 1
|
| })
|
|
|
| colour_df = pd.DataFrame(colour_rows)
|
| return colour_df, colour_details
|
|
|
| def create_detailed_colour_breakdown(colour_details: dict) -> pd.DataFrame:
|
| """Create detailed breakdown showing which orders contribute to each colour"""
|
| breakdown_rows = []
|
|
|
| for colour, details in colour_details.items():
|
| for detail in details:
|
| breakdown_rows.append({
|
| 'Colour': colour,
|
| 'Design': detail['Design'],
|
| 'Order_Number': detail['Order'],
|
| 'Kgs_for_This_Colour': round(detail['Kgs_Contribution'], 2),
|
| 'Total_Order_Kgs': detail['Total_Order_Kgs']
|
| })
|
|
|
| breakdown_df = pd.DataFrame(breakdown_rows)
|
|
|
| breakdown_df = breakdown_df.sort_values(['Colour', 'Kgs_for_This_Colour'], ascending=[True, False])
|
|
|
| return breakdown_df
|
|
|
| def detect_date_columns(df: pd.DataFrame) -> list:
|
| """Detect date columns in the dataframe"""
|
| date_columns = []
|
|
|
| for col in df.columns:
|
| col_str = str(col).strip()
|
|
|
|
|
| try:
|
| pd.to_datetime(col_str)
|
| date_columns.append(col)
|
| except:
|
|
|
| if '/' in col_str and len(col_str.split('/')) == 2:
|
| try:
|
| parts = col_str.split('/')
|
| if all(part.isdigit() for part in parts):
|
| date_columns.append(col)
|
| except:
|
| pass
|
|
|
| return date_columns
|
|
|
| def find_earliest_order_date(df: pd.DataFrame) -> pd.Series:
|
| """Find the earliest date for each order from date columns"""
|
| date_columns = detect_date_columns(df)
|
|
|
| if not date_columns:
|
|
|
| return pd.Series([365] * len(df), index=df.index)
|
|
|
| earliest_dates = []
|
|
|
| for idx, row in df.iterrows():
|
| order_dates = []
|
|
|
| for date_col in date_columns:
|
| cell_value = row[date_col]
|
|
|
|
|
| if pd.isna(cell_value) or cell_value == 0 or cell_value == "":
|
| continue
|
|
|
|
|
| try:
|
| if '/' in str(date_col):
|
|
|
| day, month = str(date_col).split('/')
|
|
|
| date_obj = pd.to_datetime(f"2025-{month.zfill(2)}-{day.zfill(2)}")
|
| else:
|
|
|
| date_obj = pd.to_datetime(str(date_col))
|
|
|
|
|
| if not pd.isna(cell_value) and str(cell_value).strip() != "" and str(cell_value) != "0":
|
| order_dates.append(date_obj)
|
|
|
| except:
|
| continue
|
|
|
|
|
| if order_dates:
|
| earliest_date = min(order_dates)
|
| else:
|
|
|
| earliest_date = pd.to_datetime("2024-01-01")
|
|
|
| earliest_dates.append(earliest_date)
|
|
|
| return pd.Series(earliest_dates, index=df.index)
|
|
|
| def compute_dyeing_priority(df: pd.DataFrame, min_kgs: int = 100, weights: dict = None) -> tuple:
|
| """
|
| Compute dyeing priority based on:
|
| 1. Oldest orders with minimum kgs per design
|
| 2. Designs with fewest colours
|
| 3. Order age
|
| """
|
|
|
|
|
| if weights is None:
|
| weights = {"AGE_WEIGHT": 50, "COLOUR_SIMPLICITY_WEIGHT": 30, "DESIGN_WEIGHT": 20}
|
|
|
| df = _normalize_columns(df)
|
|
|
|
|
| missing = [c for c in REQUIRED_COLS if c not in df.columns]
|
| if missing:
|
| raise ValueError(f"Missing required columns: {missing}. Found columns: {list(df.columns)}")
|
|
|
|
|
| out = df.copy()
|
|
|
|
|
| out["OrderDate"] = find_earliest_order_date(out)
|
|
|
|
|
| today = pd.Timestamp.now().normalize()
|
| out["OrderAgeDays"] = (today - out["OrderDate"]).dt.days
|
| out["OrderAgeDays"] = out["OrderAgeDays"].fillna(0).clip(lower=0)
|
|
|
|
|
| out["Kgs"] = pd.to_numeric(out["Kgs"], errors="coerce").fillna(0)
|
|
|
|
|
| out["ColourList"] = out["Colours"].apply(_parse_colours)
|
| out["ColourCount"] = out["ColourList"].apply(len)
|
|
|
|
|
| design_groups = out.groupby("DESIGN").agg({
|
| "Kgs": "sum",
|
| "OrderDate": "min",
|
| "OrderAgeDays": "max",
|
| "ColourCount": "first",
|
| "Order #": "count"
|
| }).reset_index()
|
|
|
| design_groups.columns = ["DESIGN", "Total_Kgs", "Oldest_Date", "Max_Age_Days", "ColourCount", "Order_Count"]
|
|
|
|
|
| design_groups["MeetsMinKgs"] = design_groups["Total_Kgs"] >= min_kgs
|
|
|
|
|
| eligible_designs = design_groups[design_groups["MeetsMinKgs"]].copy()
|
|
|
| if len(eligible_designs) == 0:
|
|
|
| eligible_designs = design_groups.copy()
|
| eligible_designs["MeetsMinKgs"] = False
|
|
|
|
|
| if eligible_designs["Max_Age_Days"].max() > 0:
|
| eligible_designs["AgeScore_01"] = eligible_designs["Max_Age_Days"] / eligible_designs["Max_Age_Days"].max()
|
| else:
|
| eligible_designs["AgeScore_01"] = 0
|
|
|
|
|
| if eligible_designs["ColourCount"].max() > 0:
|
| eligible_designs["ColourSimplicityScore_01"] = 1 - (eligible_designs["ColourCount"] / eligible_designs["ColourCount"].max())
|
| else:
|
| eligible_designs["ColourSimplicityScore_01"] = 0
|
|
|
|
|
| if eligible_designs["Total_Kgs"].max() > 0:
|
| eligible_designs["VolumeScore_01"] = eligible_designs["Total_Kgs"] / eligible_designs["Total_Kgs"].max()
|
| else:
|
| eligible_designs["VolumeScore_01"] = 0
|
|
|
|
|
| w_age = weights["AGE_WEIGHT"] / 100.0
|
| w_colour = weights["COLOUR_SIMPLICITY_WEIGHT"] / 100.0
|
| w_design = weights["DESIGN_WEIGHT"] / 100.0
|
|
|
| eligible_designs["AgeScore"] = eligible_designs["AgeScore_01"] * w_age
|
| eligible_designs["ColourSimplicityScore"] = eligible_designs["ColourSimplicityScore_01"] * w_colour
|
| eligible_designs["VolumeScore"] = eligible_designs["VolumeScore_01"] * w_design
|
|
|
| eligible_designs["PriorityScore"] = (
|
| eligible_designs["AgeScore"] +
|
| eligible_designs["ColourSimplicityScore"] +
|
| eligible_designs["VolumeScore"]
|
| )
|
|
|
|
|
| eligible_designs = eligible_designs.sort_values(
|
| ["MeetsMinKgs", "PriorityScore", "Max_Age_Days"],
|
| ascending=[False, False, False]
|
| )
|
|
|
|
|
| detailed_results = out.merge(
|
| eligible_designs[["DESIGN", "Total_Kgs", "Max_Age_Days", "MeetsMinKgs",
|
| "AgeScore", "ColourSimplicityScore", "VolumeScore", "PriorityScore"]],
|
| on="DESIGN",
|
| how="left"
|
| )
|
|
|
|
|
| detailed_results = detailed_results.sort_values(
|
| ["MeetsMinKgs", "PriorityScore", "OrderAgeDays"],
|
| ascending=[False, False, False]
|
| )
|
|
|
|
|
| colour_totals, colour_details = calculate_colour_totals(out)
|
| colour_breakdown = create_detailed_colour_breakdown(colour_details)
|
|
|
| return detailed_results, eligible_designs, colour_totals, colour_breakdown
|
|
|
| def save_dyeing_results(detailed_df, design_summary, colour_totals, colour_breakdown, output_path, min_kgs, weights):
|
| """Save all results with multiple sheets"""
|
|
|
| with pd.ExcelWriter(output_path, engine='openpyxl') as writer:
|
|
|
|
|
| colour_totals.to_excel(writer, sheet_name='COLOUR_REQUIREMENTS', index=False)
|
|
|
|
|
| colour_breakdown.to_excel(writer, sheet_name='Colour_Order_Breakdown', index=False)
|
|
|
|
|
| design_summary.to_excel(writer, sheet_name='Design_Priority_Summary', index=False)
|
|
|
|
|
| detailed_df.to_excel(writer, sheet_name='Order_Priority_Detail', index=False)
|
|
|
|
|
| instructions_data = [
|
| ['π¨ DYEING PRIORITY & COLOUR REQUIREMENTS ANALYSIS'],
|
| [''],
|
| ['π SHEET EXPLANATIONS:'],
|
| [''],
|
| ['1. COLOUR_REQUIREMENTS - π― MAIN OUTPUT YOU NEED'],
|
| [' β’ Total kgs needed for each colour (consolidated across all designs)'],
|
| [' β’ No colour repetition - each colour listed once with total quantity'],
|
| [' β’ Sorted by quantity (highest first) for production planning'],
|
| [' β’ Shows which designs use each colour and order count'],
|
| [''],
|
| ['2. Colour_Order_Breakdown - Detailed breakdown'],
|
| [' β’ Shows exactly which orders contribute to each colour total'],
|
| [' β’ Useful for tracking and verification'],
|
| [''],
|
| ['3. Design_Priority_Summary - Design-level priorities'],
|
| [' β’ Ranked by priority score for production sequence'],
|
| [''],
|
| ['4. Order_Priority_Detail - Individual order details'],
|
| [' β’ All orders with calculated priority scores'],
|
| [''],
|
| ['π― PRIORITY METHODOLOGY:'],
|
| [f'β’ Age Weight: {weights["AGE_WEIGHT"]}% - Prioritizes older orders'],
|
| [f'β’ Colour Simplicity Weight: {weights["COLOUR_SIMPLICITY_WEIGHT"]}% - Fewer colours = higher priority'],
|
| [f'β’ Design Volume Weight: {weights["DESIGN_WEIGHT"]}% - Larger quantities get priority'],
|
| [f'β’ Minimum Kgs Threshold: {min_kgs} - Only designs with total kgs >= this value are prioritized'],
|
| [''],
|
| ['π¨ COLOUR CONSOLIDATION LOGIC:'],
|
| ['β’ If RED is used in Design-A (100kg) and Design-B (50kg)'],
|
| ['β’ Output shows: RED = 150kg total (no repetition)'],
|
| ['β’ Helps plan exact dye batch quantities needed'],
|
| ['β’ Multi-colour orders split proportionally (e.g., "Red,Blue" 100kg = 50kg each)'],
|
| [''],
|
| ['π USAGE RECOMMENDATIONS:'],
|
| ['β’ Use COLOUR_REQUIREMENTS sheet for dye purchasing/batching'],
|
| ['β’ Use Design_Priority_Summary for production sequence planning'],
|
| ['β’ Check Colour_Order_Breakdown for detailed verification'],
|
| [''],
|
| [f'Generated on: {datetime.now().strftime("%Y-%m-%d %H:%M:%S")}']
|
| ]
|
|
|
| instructions_df = pd.DataFrame(instructions_data, columns=['Instructions'])
|
| instructions_df.to_excel(writer, sheet_name='Instructions', index=False)
|
|
|
|
|
| def load_excel(file):
|
| """Load Excel file and return available sheet names"""
|
| if file is None:
|
| return gr.Dropdown(choices=[]), "Please upload a file first."
|
|
|
| try:
|
| xls = pd.ExcelFile(file.name)
|
| return gr.Dropdown(choices=xls.sheet_names, value=xls.sheet_names[0]), "β
File loaded successfully!"
|
| except Exception as e:
|
| return gr.Dropdown(choices=[]), f"β Error loading file: {str(e)}"
|
|
|
| def validate_weights(age_weight, colour_weight, design_weight):
|
| """Validate that weights sum to 100%"""
|
| total = age_weight + colour_weight + design_weight
|
| if total == 100:
|
| return "β
Weights are valid (sum = 100%)"
|
| else:
|
| return f"β οΈ Weights sum to {total}%. Please adjust to equal 100%."
|
|
|
| def preview_dyeing_data(file, sheet_name):
|
| """Preview the selected sheet data for dyeing analysis"""
|
| if file is None or not sheet_name:
|
| return "Please upload a file and select a sheet first.", pd.DataFrame()
|
|
|
| try:
|
| df = pd.read_excel(file.name, sheet_name=sheet_name)
|
|
|
|
|
| preview_info = f"π **Sheet: {sheet_name}**\n"
|
| preview_info += f"- Rows: {len(df)}\n"
|
| preview_info += f"- Columns: {len(df.columns)}\n\n"
|
|
|
|
|
| df_norm = df.copy()
|
| df_norm.columns = [str(c).strip() for c in df_norm.columns]
|
| missing = [c for c in REQUIRED_COLS if c not in df_norm.columns]
|
|
|
| if missing:
|
| preview_info += f"β **Missing required columns:** {missing}\n\n"
|
| else:
|
| preview_info += "β
**All required columns found!**\n\n"
|
|
|
|
|
| date_columns = detect_date_columns(df_norm)
|
| if date_columns:
|
| preview_info += f"π
**Date columns detected:** {len(date_columns)} columns\n"
|
| preview_info += f" Sample dates: {date_columns[:5]}\n\n"
|
| else:
|
| preview_info += "β οΈ **No date columns detected** - will use default prioritization\n\n"
|
|
|
|
|
| if 'Kgs' in df_norm.columns:
|
| total_kgs = pd.to_numeric(df_norm['Kgs'], errors='coerce').sum()
|
| preview_info += f"**Total Kgs:** {total_kgs:,.1f}\n"
|
|
|
| if 'DESIGN' in df_norm.columns:
|
| unique_designs = df_norm['DESIGN'].nunique()
|
| preview_info += f"**Unique Designs:** {unique_designs}\n"
|
|
|
| preview_info += f"\n**Available columns:**\n"
|
| for i, col in enumerate(df.columns, 1):
|
| marker = "π
" if col in date_columns else ""
|
| preview_info += f"{i}. {col} {marker}\n"
|
|
|
|
|
| preview_df = df.head(5)
|
|
|
| return preview_info, preview_df
|
|
|
| except Exception as e:
|
| return f"β Error previewing data: {str(e)}", pd.DataFrame()
|
|
|
| def process_dyeing_priority(file, sheet_name, age_weight, colour_weight, design_weight, min_kgs):
|
| """Main processing function for dyeing priorities"""
|
|
|
| if file is None:
|
| return None, None, None, "β Please upload a file first."
|
|
|
| if not sheet_name:
|
| return None, None, None, "β Please select a sheet."
|
|
|
|
|
| total_weight = age_weight + colour_weight + design_weight
|
| if total_weight != 100:
|
| return None, None, None, f"β Error: Total weight must equal 100% (currently {total_weight}%)"
|
|
|
| try:
|
|
|
| df = pd.read_excel(file.name, sheet_name=sheet_name)
|
|
|
| if df.empty:
|
| return None, None, None, "β The selected sheet is empty."
|
|
|
|
|
| weights = {
|
| "AGE_WEIGHT": age_weight,
|
| "COLOUR_SIMPLICITY_WEIGHT": colour_weight,
|
| "DESIGN_WEIGHT": design_weight
|
| }
|
|
|
|
|
| detailed_results, design_summary, colour_totals, colour_breakdown = compute_dyeing_priority(
|
| df, min_kgs=min_kgs, weights=weights
|
| )
|
|
|
|
|
| output_path = tempfile.NamedTemporaryFile(delete=False, suffix='.xlsx').name
|
| save_dyeing_results(detailed_results, design_summary, colour_totals, colour_breakdown, output_path, min_kgs, weights)
|
|
|
|
|
| total_designs = len(design_summary)
|
| eligible_designs = sum(design_summary['MeetsMinKgs'])
|
| total_colours = len(colour_totals)
|
| top_colours = colour_totals.head(3)['Colour'].tolist() if len(colour_totals) > 0 else []
|
|
|
| success_msg = f"β
Dyeing Priority Analysis Complete!\n"
|
| success_msg += f"π SUMMARY:\n"
|
| success_msg += f"- Total Designs Analyzed: {total_designs}\n"
|
| success_msg += f"- Designs Meeting {min_kgs}kg Threshold: {eligible_designs}\n"
|
| success_msg += f"- Unique Colours Required: {total_colours}\n"
|
| if top_colours:
|
| success_msg += f"- Top 3 Colours by Volume: {', '.join(top_colours)}\n"
|
| success_msg += f"- Highest Priority Score: {design_summary['PriorityScore'].max():.3f}\n\n"
|
| success_msg += f"π¨ COLOUR REQUIREMENTS sheet contains consolidated totals!\n"
|
| success_msg += f"π₯ Download complete analysis below"
|
|
|
| return output_path, design_summary.head(10), colour_totals.head(15), success_msg
|
|
|
| except Exception as e:
|
| return None, None, None, f"β Error processing data: {str(e)}"
|
|
|
|
|
| def create_dyeing_interface():
|
| with gr.Blocks(title="Dyeing Urgency Priority Calculator", theme=gr.themes.Soft()) as demo:
|
|
|
| gr.Markdown("""
|
| # π¨ Dyeing Urgency Priority Calculator
|
|
|
| Upload your Excel file with dyeing/textile manufacturing data to calculate production priorities based on:
|
| - **Order Age**: Prioritize older orders first (detects dates from column headers)
|
| - **Colour Simplicity**: Fewer colours = easier production
|
| - **Design Volume**: Larger quantities for efficiency
|
|
|
| **Expected Columns**: Account, Order #, DESIGN, Labels, Colours, Kgs, Pending
|
| **Date Detection**: Automatically detects date columns (like 2025-01-08, 13/8, etc.)
|
| """)
|
|
|
| with gr.Row():
|
| with gr.Column(scale=1):
|
| gr.Markdown("## π File Upload & Selection")
|
|
|
| file_input = gr.File(
|
| label="Upload Excel File",
|
| file_types=[".xlsx", ".xls"],
|
| type="filepath"
|
| )
|
|
|
| sheet_dropdown = gr.Dropdown(
|
| label="Select Sheet",
|
| choices=[],
|
| interactive=True
|
| )
|
|
|
| file_status = gr.Textbox(label="File Status", interactive=False)
|
|
|
| with gr.Column(scale=1):
|
| gr.Markdown("## βοΈ Priority Weights (must sum to 100%)")
|
|
|
| age_weight = gr.Slider(
|
| minimum=0, maximum=100, value=50, step=1,
|
| label="Age Weight (%)",
|
| info="Higher = prioritize older orders more"
|
| )
|
|
|
| colour_weight = gr.Slider(
|
| minimum=0, maximum=100, value=30, step=1,
|
| label="Colour Simplicity Weight (%)",
|
| info="Higher = prioritize designs with fewer colours"
|
| )
|
|
|
| design_weight = gr.Slider(
|
| minimum=0, maximum=100, value=20, step=1,
|
| label="Design Volume Weight (%)",
|
| info="Higher = prioritize larger quantity designs"
|
| )
|
|
|
| weight_status = gr.Textbox(label="Weight Validation", interactive=False)
|
|
|
| min_kgs = gr.Number(
|
| label="Minimum Kgs Threshold per Design",
|
| value=100,
|
| info="Only designs with total kgs >= this value get priority"
|
| )
|
|
|
| with gr.Row():
|
| preview_btn = gr.Button("ποΈ Preview Data", variant="secondary")
|
| process_btn = gr.Button("π¨ Calculate Dyeing Priorities", variant="primary", size="lg")
|
|
|
| with gr.Row():
|
| with gr.Column():
|
| gr.Markdown("## π Data Preview")
|
| preview_info = gr.Textbox(label="Data Information", lines=10, interactive=False)
|
| preview_table = gr.Dataframe(label="Sample Data")
|
|
|
| with gr.Row():
|
| with gr.Column():
|
| gr.Markdown("## π Priority Results")
|
| results_info = gr.Textbox(label="Processing Status", interactive=False)
|
|
|
| with gr.Column():
|
| download_file = gr.File(label="π₯ Download Complete Analysis")
|
|
|
| with gr.Row():
|
| with gr.Column():
|
| gr.Markdown("## π Top Design Priorities")
|
| design_results = gr.Dataframe(label="Design Priority Summary")
|
|
|
| with gr.Column():
|
| gr.Markdown("## π¨ Colour Requirements (Consolidated)")
|
| colour_results = gr.Dataframe(
|
| label="Total Kgs Required Per Colour",
|
| headers=["Colour", "Total Kgs", "Used in Designs", "Orders Count"],
|
| interactive=False
|
| )
|
|
|
|
|
| file_input.change(
|
| fn=load_excel,
|
| inputs=[file_input],
|
| outputs=[sheet_dropdown, file_status]
|
| )
|
|
|
| for weight_input in [age_weight, colour_weight, design_weight]:
|
| weight_input.change(
|
| fn=validate_weights,
|
| inputs=[age_weight, colour_weight, design_weight],
|
| outputs=[weight_status]
|
| )
|
|
|
| preview_btn.click(
|
| fn=preview_dyeing_data,
|
| inputs=[file_input, sheet_dropdown],
|
| outputs=[preview_info, preview_table]
|
| )
|
|
|
| process_btn.click(
|
| fn=process_dyeing_priority,
|
| inputs=[file_input, sheet_dropdown, age_weight, colour_weight, design_weight, min_kgs],
|
| outputs=[download_file, design_results, colour_results, results_info]
|
| )
|
|
|
|
|
| demo.load(
|
| fn=validate_weights,
|
| inputs=[age_weight, colour_weight, design_weight],
|
| outputs=[weight_status]
|
| )
|
|
|
| return demo
|
|
|
|
|
| if __name__ == "__main__":
|
| demo = create_dyeing_interface()
|
| demo.launch(
|
|
|
|
|
| share=True,
|
| debug=True
|
| ) |