From 30392a7cd979dff0d2377bad74b1c20101b4795e Mon Sep 17 00:00:00 2001 From: Mann Patel <130435633+Patel-Mann@users.noreply.github.com> Date: Sun, 15 Feb 2026 02:18:07 -0700 Subject: [PATCH] feat: not working download zip file --- .../__pycache__/api_routes.cpython-314.pyc | Bin 32124 -> 34373 bytes .../__pycache__/db_queries.cpython-314.pyc | Bin 25324 -> 28576 bytes backend/api_routes.py | 159 ++++++------------ 3 files changed, 47 insertions(+), 112 deletions(-) diff --git a/backend/__pycache__/api_routes.cpython-314.pyc b/backend/__pycache__/api_routes.cpython-314.pyc index 1ffef2e4734c213baf6b207a18a350a67d1f53f8..2c72bd919d48646c08a804ff668e042a1097e41b 100644 GIT binary patch delta 9095 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 H99aJY3WzJP diff --git a/backend/__pycache__/db_queries.cpython-314.pyc b/backend/__pycache__/db_queries.cpython-314.pyc index f8f6ee0e31c80cd3dccd5deb309403b1f7dc31ef..e072a9891f91d56a71ceae12257b1d00e53f2f23 100644 GIT binary patch delta 5313 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 z zN;F7$CX-3zX-XEcXy)o8INq?qYo{7yf+t87fmKrC@pxzk3SEIK`!i`_fay>Vc1==q_grVBY$*Vm@3b^Xg~w>!SD3a*TQX0g|YF1UF|cwnPh{Cr06 zY>`3zo#7Eua1Le3jGqP4>`3|oEU`1#&e0s!Ig&440>+@eXC#}z%ztX<5DDm)cm9x& zb8PZ~qXT~csX>Y<_Vl3H(~kc!r$gb;j6!*TzXUsOYw{HS{?d=(+->4zkOkP{=n;7I zAHV&j)bu0cemHk#~#MA82?m@j?`W;Fe3@QKwObh3{e&_9?wO}eh& z`N=E5H|(}3%zH>Q7?*iaAs?X(!He)LTdK)sf0qfe2;41v)cK=H@nNF)2AHnm#5DcIsbe~gRcv~WxuuvH zhyjkG=r(l&z&Fx0_@`o%`psTQZH~l(vtgx`-T)>FR402h`nqM5u&P*ZK8% str: f"{transcript_text}\n\n" "Answer user questions grounded in this transcript." ) -def _add_audio_url(post: dict[str, Any]) -> dict[str, Any]: +def _add_audio_url(post: Dict[str, Any]) -> Dict[str, Any]: """Add signed audio URL to post if ready""" if post.get("status") == "ready": try: @@ -114,29 +111,6 @@ def _add_audio_url(post: dict[str, Any]) -> dict[str, Any]: return post -def _local_embedding(text: str, dimensions: int = 1536) -> List[float]: - """ - Free deterministic embedding fallback (offline). - Replace with model-based embeddings later if needed. - """ - vector = [0.0] * dimensions - tokens = re.findall(r"[A-Za-z0-9']+", text.lower()) - if not tokens: - return vector - - for token in tokens: - digest = hashlib.sha256(token.encode("utf-8")).digest() - idx = int.from_bytes(digest[:4], "big") % dimensions - sign = 1.0 if (digest[4] & 1) == 0 else -1.0 - weight = 1.0 + (digest[5] / 255.0) * 0.25 - vector[idx] += sign * weight - - norm = sum(v * v for v in vector) ** 0.5 - if norm > 0: - vector = [v / norm for v in vector] - return vector - - @api.get("/health") def health(): @@ -304,7 +278,7 @@ def api_upload_post(): "end_sec": float(seg.end), "text": segment_text, "confidence": float(seg.avg_logprob) if seg.avg_logprob is not None else None, - "embedding": _local_embedding(segment_text), + "embedding": None, } ) @@ -390,31 +364,17 @@ def api_user_history(user_id: int): def api_rag_search(): query_text = (request.args.get("q") or "").strip() user_id = request.args.get("user_id", type=int) - query_embedding_raw = request.args.get("query_embedding") page = request.args.get("page", default=1, type=int) limit = request.args.get("limit", default=30, type=int) if not user_id: return _error("'user_id' is required.", 400) + if not query_text: + return _error("'q' is required.", 400) try: - if query_embedding_raw: - try: - parsed = json.loads(query_embedding_raw) - if not isinstance(parsed, list): - return _error("'query_embedding' must be a JSON array.", 400) - query_embedding = [float(v) for v in parsed] - except Exception: - return _error("Invalid 'query_embedding'. Example: [0.1,0.2,...]", 400) - - rows = search_rag_chunks_vector(user_id=user_id, query_embedding=query_embedding, limit=limit) - return jsonify({"results": rows, "mode": "vector", "limit": min(max(1, limit), 100)}) - - if not query_text: - return _error("'q' is required when 'query_embedding' is not provided.", 400) - rows = search_rag_chunks(user_id=user_id, query_text=query_text, page=page, limit=limit) - return jsonify({"results": rows, "mode": "text", "page": page, "limit": min(max(1, limit), 100)}) + return jsonify({"results": rows, "page": page, "limit": min(max(1, limit), 100)}) except Exception as e: return _error(str(e), 500) @@ -458,19 +418,19 @@ def api_list_posts(): limit = request.args.get("limit", default=20, type=int) visibility = request.args.get("visibility") current_user_id = request.args.get("current_user_id", type=int) # NEW LINE - + try: rows = list_audio_posts(page=page, limit=limit, visibility=visibility) - + # NEW: Filter private posts if current_user_id: rows = [p for p in rows if p.get('visibility') == 'public' or p.get('user_id') == current_user_id] else: rows = [p for p in rows if p.get('visibility') == 'public'] - + # NEW: Add audio URLs - CHANGE THIS LINE ONLY rows = [_add_audio_url(post) for post in rows] - + return jsonify({"posts": rows, "page": page, "limit": min(max(1, limit), 100)}) except Exception as e: return _error(str(e), 500) @@ -533,64 +493,6 @@ def api_post_audio_url(post_id: int): return _error(str(e), 500) -@api.get("/posts//archive.zip") -def api_post_archive_zip(post_id: int): - """ - Download a complete archive zip for a post. - Private posts require owner user_id in query params. - """ - row = get_audio_post_by_id(post_id) - if not row: - return _error("Post not found.", 404) - - visibility = row.get("visibility") - owner_id = row.get("user_id") - requester_id = request.args.get("user_id", type=int) - - if visibility == "private" and requester_id != owner_id: - return _error("Not authorized to download this private archive.", 403) - - try: - bundle = get_post_bundle(post_id) - if not bundle: - return _error("Post bundle not found.", 404) - - archive_buf = io.BytesIO() - with zipfile.ZipFile(archive_buf, mode="w", compression=zipfile.ZIP_DEFLATED) as zf: - # Always include core JSON artifacts. - zf.writestr("post.json", json.dumps(bundle.get("post", {}), indent=2, default=str)) - zf.writestr("metadata.json", json.dumps(bundle.get("metadata", {}), indent=2, default=str)) - zf.writestr("rights.json", json.dumps(bundle.get("rights", {}), indent=2, default=str)) - zf.writestr("rag_chunks.json", json.dumps(bundle.get("rag_chunks", []), indent=2, default=str)) - zf.writestr("audit_log.json", json.dumps(bundle.get("audit_log", []), indent=2, default=str)) - - # Derive transcript from rag chunks. - chunks = bundle.get("rag_chunks", []) or [] - transcript_text = " ".join( - (chunk.get("text") or "").strip() for chunk in chunks if chunk.get("text") - ).strip() - if transcript_text: - zf.writestr("transcript.txt", transcript_text) - - # Include original media from Supabase Storage if present. - original_file = get_archive_file_by_role(post_id, "original_audio") - if original_file and original_file.get("path"): - original_bytes = download_storage_object_by_stored_path(original_file["path"]) - source_name = original_file["path"].split("/")[-1] or f"post_{post_id}_original.bin" - zf.writestr(f"original/{source_name}", original_bytes) - - archive_buf.seek(0) - download_name = f"voicevault_post_{post_id}_archive.zip" - return send_file( - archive_buf, - mimetype="application/zip", - as_attachment=True, - download_name=download_name, - ) - except Exception as e: - return _error(f"Failed to build archive zip: {e}", 500) - - @api.post("/posts//files") def api_add_file(post_id: int): payload = request.get_json(force=True, silent=False) or {} @@ -707,3 +609,36 @@ def api_post_audit(post_id: int): return jsonify({"logs": list_audit_logs(post_id=post_id, page=page, limit=limit)}) except Exception as e: return _error(str(e), 500) + +@api.get("/posts//download") +def download_post(post_id: int): + post = get_audio_post_by_id(post_id) + if not post: + return jsonify({"error": "Post not found"}), 404 + + files = list_archive_files(post_id) + metadata = get_archive_metadata(post_id) or {} + + zip_buffer = io.BytesIO() + with zipfile.ZipFile(zip_buffer, "w", zipfile.ZIP_DEFLATED) as zipf: + if metadata.get("metadata"): + zipf.writestr("metadata.json", json.dumps(metadata, indent=2)) + + for f in files: + try: + signed_url = get_original_audio_url(post_id)["signed_url"] if f["role"] == "original_audio" else None + if signed_url: + r = requests.get(signed_url) + if r.status_code == 200: + filename = f"{f['role']}_{f['path'].split('/')[-1]}" + zipf.writestr(filename, r.content) + except Exception as e: + print("Failed to add file:", e) + + zip_buffer.seek(0) + return send_file( + zip_buffer, + mimetype="application/zip", + as_attachment=True, + download_name=f"{post['title'].replace(' ', '_')}.zip" + )