Spaces:
Sleeping
Sleeping
| import os, json, gc, pickle, traceback, zipfile, shutil, uuid, logging | |
| from datetime import datetime | |
| from flask import Flask, render_template, request, jsonify, send_file | |
| from werkzeug.utils import secure_filename | |
| import numpy as np | |
| import faiss | |
| logging.basicConfig(level=logging.INFO) | |
| logger = logging.getLogger(__name__) | |
| app = Flask(__name__) | |
| app.config['MAX_CONTENT_LENGTH'] = 500 * 1024 * 1024 # 500 MB | |
| app.config['TEMP_FOLDER'] = os.path.join(os.path.expanduser('~'), 'tmp_merge') | |
| os.makedirs(app.config['TEMP_FOLDER'], exist_ok=True) | |
| def merge_page(): | |
| return render_template('merge.html') | |
| def merge_zips(): | |
| zip_files = request.files.getlist('files') | |
| out_path = request.form.get('out_path', '/tmp').strip() | |
| out_name = request.form.get('out_name', 'merged_vectorstore').strip() | |
| if out_name.lower().endswith('.zip'): | |
| out_name = out_name[:-4] | |
| if not zip_files: | |
| return jsonify({'error': 'No files uploaded'}), 400 | |
| for f in zip_files: | |
| if not f.filename.lower().endswith('.zip'): | |
| return jsonify({'error': f'{f.filename} is not a .zip file'}), 400 | |
| os.makedirs(out_path, exist_ok=True) | |
| run_id = uuid.uuid4().hex[:8] | |
| work_dir = os.path.join(app.config['TEMP_FOLDER'], f'merge_{run_id}') | |
| os.makedirs(work_dir, exist_ok=True) | |
| main_index = None | |
| all_documents = [] | |
| all_metadata = [] | |
| embedding_dim = None | |
| total_merged = 0 | |
| errors, skipped = [], [] | |
| try: | |
| for idx, zip_file in enumerate(zip_files, 1): | |
| safe_name = secure_filename(zip_file.filename) | |
| zip_save = os.path.join(work_dir, safe_name) | |
| extract_dir = os.path.join(work_dir, f'ex_{idx:02d}') | |
| os.makedirs(extract_dir, exist_ok=True) | |
| zip_file.save(zip_save) | |
| if not zipfile.is_zipfile(zip_save): | |
| errors.append(f'{safe_name}: not a valid zip β skipped') | |
| continue | |
| with zipfile.ZipFile(zip_save, 'r') as zf: | |
| zf.extractall(extract_dir) | |
| # Locate the three required files | |
| index_file = data_file = meta_file = None | |
| for root, dirs, fnames in os.walk(extract_dir): | |
| for fn in fnames: | |
| full = os.path.join(root, fn) | |
| if fn == 'faiss.index' and index_file is None: index_file = full | |
| if fn == 'data.pkl' and data_file is None: data_file = full | |
| if fn == 'meta.json' and meta_file is None: meta_file = full | |
| if not index_file or not data_file: | |
| errors.append(f'{safe_name}: missing faiss.index/data.pkl β skipped') | |
| continue | |
| # Load meta.json (optional, we can create it later) | |
| meta = {} | |
| if meta_file: | |
| with open(meta_file, 'r') as f: | |
| meta = json.load(f) | |
| try: | |
| sub_index = faiss.read_index(index_file) | |
| with open(data_file, 'rb') as f: | |
| sub_data = pickle.load(f) | |
| except Exception as e: | |
| errors.append(f'{safe_name}: load error β {e}') | |
| continue | |
| sub_docs = sub_data.get('documents', []) | |
| sub_meta = sub_data.get('metadata', []) | |
| n = sub_index.ntotal | |
| if n == 0: | |
| skipped.append(f'{safe_name}: empty index β skipped') | |
| continue | |
| if n != len(sub_docs) or n != len(sub_meta): | |
| errors.append(f'{safe_name}: index/pkl mismatch ({n} vectors, {len(sub_docs)} docs, {len(sub_meta)} meta) β skipped') | |
| continue | |
| # Dimension check | |
| dim = sub_index.d | |
| if embedding_dim is None: | |
| embedding_dim = dim | |
| main_index = faiss.IndexFlatL2(dim) | |
| elif dim != embedding_dim: | |
| errors.append(f'{safe_name}: dimension mismatch (got {dim}, expected {embedding_dim}) β skipped') | |
| continue | |
| # Extract vectors | |
| vectors = np.zeros((n, dim), dtype=np.float32) | |
| sub_index.reconstruct_n(0, n, vectors) | |
| main_index.add(vectors) | |
| all_documents.extend(sub_docs) | |
| all_metadata.extend(sub_meta) | |
| total_merged += n | |
| logger.info(f' β {safe_name}: +{n} vectors (total {total_merged})') | |
| del sub_index, vectors, sub_docs, sub_meta, sub_data | |
| gc.collect() | |
| if total_merged == 0: | |
| return jsonify({'error': 'No vectors merged', 'errors': errors, 'skipped': skipped}), 400 | |
| # Prepare output zip | |
| merged_dir = os.path.join(work_dir, 'merged') | |
| os.makedirs(merged_dir, exist_ok=True) | |
| # Write faiss.index | |
| index_out = os.path.join(merged_dir, 'faiss.index') | |
| faiss.write_index(main_index, index_out) | |
| # Write data.pkl | |
| data_out = os.path.join(merged_dir, 'data.pkl') | |
| with open(data_out, 'wb') as f: | |
| pickle.dump({'documents': all_documents, 'metadata': all_metadata}, f) | |
| # Write fresh meta.json | |
| new_meta = { | |
| 'version': 2, | |
| 'similarity': 'cosine', # safe default; adjust if you know the real one | |
| 'embedding_dim': embedding_dim, | |
| 'chunk_count': total_merged | |
| } | |
| meta_out = os.path.join(merged_dir, 'meta.json') | |
| with open(meta_out, 'w') as f: | |
| json.dump(new_meta, f) | |
| # Package into zip | |
| final_zip = os.path.join(out_path, out_name + '.zip') | |
| with zipfile.ZipFile(final_zip, 'w', zipfile.ZIP_DEFLATED) as zf: | |
| zf.write(index_out, 'faiss.index') | |
| zf.write(data_out, 'data.pkl') | |
| zf.write(meta_out, 'meta.json') | |
| return send_file(final_zip, as_attachment=True, download_name=os.path.basename(final_zip)) | |
| except Exception as e: | |
| logger.error(f'Merge error: {e}') | |
| logger.error(traceback.format_exc()) | |
| return jsonify({'error': str(e)}), 500 | |
| finally: | |
| if os.path.exists(work_dir): | |
| shutil.rmtree(work_dir, ignore_errors=True) | |
| if __name__ == '__main__': | |
| port = int(os.environ.get('PORT', 7860)) | |
| logger.info(f'β Starting merge server on port {port}') | |
| app.run(host='0.0.0.0', port=port, debug=False) |