Update app.py
This commit is contained in:
@@ -1,26 +1,31 @@
|
||||
# api.py — OCI GenAI + OpenAI v1 Compatibility
|
||||
# api.py — OCI GenAI + OpenAI v1 Compatibility (files + images + multimodal)
|
||||
# -----------------------------------------------------------------------------
|
||||
# Requisitos:
|
||||
# pip install flask oci requests
|
||||
# pip install flask oci requests pillow
|
||||
# Execução:
|
||||
# export API_KEY="minha-chave"
|
||||
# export GENAI_BUCKET="lohmann-ai-br"
|
||||
# export GENAI_UPLOAD_PREFIX="genai-uploads/"
|
||||
# python api.py # porta 8000
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
from flask import Flask, request, jsonify, abort, Response, stream_with_context
|
||||
from flask import Flask, request, jsonify, abort, Response, stream_with_context, send_file
|
||||
import oci
|
||||
import requests
|
||||
import os
|
||||
import io
|
||||
import json
|
||||
import uuid
|
||||
import base64
|
||||
import time
|
||||
import mimetypes
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Dict, List, Optional, Generator
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# ==========================
|
||||
# Configuração
|
||||
# Configuração e Autenticação OCI
|
||||
# ==========================
|
||||
|
||||
def load_config(config_file="/home/app/credentials.conf"):
|
||||
@@ -57,6 +62,118 @@ if not TEST_MODE:
|
||||
print("Executando em modo de teste...")
|
||||
TEST_MODE = True
|
||||
|
||||
# ==========================
|
||||
# Variáveis de Bucket / Uploads
|
||||
# ==========================
|
||||
|
||||
BUCKET_NAME = os.environ.get("GENAI_BUCKET", "lohmann-ai-br")
|
||||
UPLOAD_PREFIX = os.environ.get("GENAI_UPLOAD_PREFIX", "genai-uploads/")
|
||||
if UPLOAD_PREFIX and not UPLOAD_PREFIX.endswith("/"):
|
||||
UPLOAD_PREFIX += "/"
|
||||
|
||||
object_client = None
|
||||
namespace = None
|
||||
region = config.get("region") or os.environ.get("OCI_REGION", "us-chicago-1")
|
||||
|
||||
if not TEST_MODE:
|
||||
try:
|
||||
object_client = oci.object_storage.ObjectStorageClient(config)
|
||||
namespace = object_client.get_namespace().data
|
||||
except Exception as e:
|
||||
print(f"Erro ao inicializar ObjectStorageClient: {e}")
|
||||
TEST_MODE = True
|
||||
|
||||
# Session store para mapear file_id -> object_name (para fallback de download)
|
||||
FILE_INDEX: Dict[str, str] = {}
|
||||
|
||||
# ==========================
|
||||
# Segurança API
|
||||
# ==========================
|
||||
|
||||
def check_api_key():
|
||||
expected_key = os.environ.get("API_KEY")
|
||||
if not expected_key:
|
||||
print("AVISO: API_KEY não configurada nas variáveis de ambiente.")
|
||||
return
|
||||
provided_key = request.headers.get("X-API-Key")
|
||||
if provided_key != expected_key:
|
||||
abort(401, description="Chave de API inválida ou ausente.")
|
||||
|
||||
@app.before_request
|
||||
def before_all_requests():
|
||||
check_api_key()
|
||||
|
||||
# ==========================
|
||||
# Helpers: Signed URL (PAR) + Upload
|
||||
# ==========================
|
||||
|
||||
def guess_mime(filename: str, default: str = "application/octet-stream") -> str:
|
||||
mt, _ = mimetypes.guess_type(filename)
|
||||
return mt or default
|
||||
|
||||
def create_par_for_object(object_name: str, hours_valid: int = 1) -> str:
|
||||
"""
|
||||
Cria um Pre-Authenticated Request (PAR) para leitura do objeto.
|
||||
Retorna a URL completa (https://objectstorage.region.oraclecloud.com{accessUri})
|
||||
"""
|
||||
if TEST_MODE:
|
||||
return f"https://objectstorage.{region}.oraclecloud.com/test/{object_name}"
|
||||
expires = datetime.utcnow() + timedelta(hours=hours_valid)
|
||||
details = oci.object_storage.models.CreatePreauthenticatedRequestDetails(
|
||||
name=f"par-{uuid.uuid4().hex[:8]}",
|
||||
access_type="ObjectRead",
|
||||
time_expires=expires,
|
||||
bucket_listing_action=None,
|
||||
object_name=object_name
|
||||
)
|
||||
par = object_client.create_preauthenticated_request(
|
||||
namespace_name=namespace,
|
||||
bucket_name=BUCKET_NAME,
|
||||
create_preauthenticated_request_details=details
|
||||
).data
|
||||
|
||||
# access_uri começa com /p/...
|
||||
base = f"https://objectstorage.{region}.oraclecloud.com"
|
||||
return base + par.access_uri
|
||||
|
||||
def upload_file_to_bucket(file_storage, filename: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Faz upload do arquivo para o bucket e retorna metadata + signed URL (PAR).
|
||||
"""
|
||||
file_storage.stream.seek(0)
|
||||
content = file_storage.read()
|
||||
size = len(content)
|
||||
if TEST_MODE:
|
||||
file_id = f"file-{uuid.uuid4().hex[:12]}"
|
||||
url = f"https://objectstorage.{region}.oraclecloud.com/test/{UPLOAD_PREFIX}{file_id}_{filename}"
|
||||
FILE_INDEX[file_id] = f"{UPLOAD_PREFIX}{file_id}_{filename}"
|
||||
return {"id": file_id, "object": "file", "filename": filename, "bytes": size, "url": url}
|
||||
|
||||
object_name = f"{UPLOAD_PREFIX}{uuid.uuid4().hex}_{filename}"
|
||||
object_client.put_object(
|
||||
namespace,
|
||||
BUCKET_NAME,
|
||||
object_name,
|
||||
content,
|
||||
content_type=guess_mime(filename, "application/octet-stream")
|
||||
)
|
||||
url = create_par_for_object(object_name, hours_valid=24)
|
||||
file_id = f"file-{uuid.uuid4().hex[:12]}"
|
||||
FILE_INDEX[file_id] = object_name
|
||||
return {
|
||||
"id": file_id,
|
||||
"object": "file",
|
||||
"filename": filename,
|
||||
"bytes": size,
|
||||
"url": url
|
||||
}
|
||||
|
||||
def get_signed_url_from_file_id(file_id: str, hours_valid: int = 24) -> Optional[str]:
|
||||
if file_id in FILE_INDEX:
|
||||
obj = FILE_INDEX[file_id]
|
||||
return create_par_for_object(obj, hours_valid=hours_valid) if not TEST_MODE else f"https://test/{obj}"
|
||||
return None
|
||||
|
||||
# ==========================
|
||||
# Modelos suportados (defaults)
|
||||
# ==========================
|
||||
@@ -95,7 +212,7 @@ SUPPORTED_MODELS: Dict[str, Dict[str, Any]] = {
|
||||
# ==========================
|
||||
|
||||
SESSION_STORE = {}
|
||||
SESSION_TTL = timedelta(hours=1)
|
||||
SESSION_TTL = timedelta(hours=2)
|
||||
|
||||
def session_controller(region, agent_endpoint_id, channel, cuid):
|
||||
"""
|
||||
@@ -234,7 +351,7 @@ def call_inference_model(region, compartment_id, model_id, prompt):
|
||||
return {"error": str(e)}
|
||||
|
||||
# ==========================
|
||||
# Utilitários (OpenAI v1 compat)
|
||||
# Utilitários (OpenAI v1)
|
||||
# ==========================
|
||||
|
||||
ROLE_MAP = {
|
||||
@@ -243,6 +360,29 @@ ROLE_MAP = {
|
||||
"assistant": "ASSISTANT",
|
||||
}
|
||||
|
||||
def ensure_data_url(image_url: str) -> str:
|
||||
"""
|
||||
Garante que a imagem seja uma data URL (base64).
|
||||
Se já for data: retorna como está.
|
||||
Se for http(s): baixa, infere MIME e converte para data URL.
|
||||
"""
|
||||
if not image_url:
|
||||
return image_url
|
||||
if image_url.startswith("data:"):
|
||||
return image_url
|
||||
try:
|
||||
resp = requests.get(image_url, timeout=30)
|
||||
resp.raise_for_status()
|
||||
content = resp.content
|
||||
# tenta inferir mime pelo header; fallback pela extensão
|
||||
mime = resp.headers.get("Content-Type") or guess_mime(image_url, "image/jpeg")
|
||||
b64 = base64.b64encode(content).decode("utf-8")
|
||||
return f"data:{mime};base64,{b64}"
|
||||
except Exception as e:
|
||||
print(f"[warn] Falha ao baixar imagem '{image_url}': {e}")
|
||||
# retorna URL original (alguns modelos podem aceitar URL remota)
|
||||
return image_url
|
||||
|
||||
def resolve_model_and_params(body: Dict[str, Any], path_model_id: str) -> Dict[str, Any]:
|
||||
"""
|
||||
Resolve o OCID do modelo a partir de:
|
||||
@@ -287,36 +427,49 @@ def resolve_model_and_params(body: Dict[str, Any], path_model_id: str) -> Dict[s
|
||||
|
||||
def to_oci_messages(openai_messages: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Converte mensagens no formato OpenAI para o formato da OCI.
|
||||
Converte mensagens no formato OpenAI para um payload genérico (que depois vira objetos do SDK).
|
||||
Suporta:
|
||||
- content string
|
||||
- content list com {type:"text", text:"..."} e {type:"image_url", image_url:{url:"..."}}
|
||||
"""
|
||||
oci_msgs: List[Dict[str, Any]] = []
|
||||
for m in openai_messages:
|
||||
role = ROLE_MAP.get(str(m.get("role", "")).lower(), "USER")
|
||||
content = m.get("content", "")
|
||||
if isinstance(content, list):
|
||||
text_parts = []
|
||||
for p in content:
|
||||
if isinstance(p, dict) and p.get("type") == "text":
|
||||
text_parts.append(p.get("text", ""))
|
||||
elif isinstance(p, str):
|
||||
text_parts.append(p)
|
||||
content_str = "\n".join([t for t in text_parts if t])
|
||||
elif isinstance(content, str):
|
||||
content_str = content
|
||||
else:
|
||||
content_str = str(content)
|
||||
|
||||
oci_msgs.append({
|
||||
"role": role,
|
||||
"content": [
|
||||
{"type": "TEXT", "text": content_str}
|
||||
]
|
||||
})
|
||||
parts: List[Dict[str, Any]] = []
|
||||
if isinstance(content, list):
|
||||
for p in content:
|
||||
# TEXT
|
||||
if isinstance(p, dict) and p.get("type") == "text":
|
||||
txt = p.get("text", "")
|
||||
if txt:
|
||||
parts.append({"type": "TEXT", "text": txt})
|
||||
# IMAGE
|
||||
elif isinstance(p, dict) and p.get("type") == "image_url":
|
||||
url = ""
|
||||
if isinstance(p.get("image_url"), dict):
|
||||
url = p.get("image_url", {}).get("url", "")
|
||||
elif isinstance(p.get("image_url"), str):
|
||||
url = p.get("image_url")
|
||||
if url:
|
||||
data_url = ensure_data_url(url)
|
||||
parts.append({"type": "IMAGE_URL", "url": data_url})
|
||||
# strings soltas tratadas como texto
|
||||
elif isinstance(p, str):
|
||||
parts.append({"type": "TEXT", "text": p})
|
||||
elif isinstance(content, str):
|
||||
parts.append({"type": "TEXT", "text": content})
|
||||
else:
|
||||
# fallback serialization
|
||||
parts.append({"type": "TEXT", "text": json.dumps(content, ensure_ascii=False)})
|
||||
|
||||
oci_msgs.append({"role": role, "content": parts})
|
||||
return oci_msgs
|
||||
|
||||
def build_oci_chat_payload(messages: List[Dict[str, Any]], params: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Monta o payload para /actions/chat da OCI.
|
||||
Monta o payload para /actions/chat da OCI (genérico).
|
||||
"""
|
||||
payload = {"messages": messages}
|
||||
|
||||
@@ -344,12 +497,13 @@ def build_oci_chat_payload(messages: List[Dict[str, Any]], params: Dict[str, Any
|
||||
def oci_chat_invoke(region: str, compartment_id: str, model_ocid: str, oci_payload: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Invoca o /actions/chat da OCI. Em TEST_MODE retorna dry-run.
|
||||
Converte conteúdo TEXT/IMAGE_URL em TextContent/ImageContent do SDK.
|
||||
"""
|
||||
# DEBUG: imprimir o payload que vai para a OCI (útil para validar 'role')
|
||||
print(">>> OCI CHAT REQUEST (payload que será enviado):")
|
||||
print(json.dumps(oci_payload, ensure_ascii=False, indent=2))
|
||||
|
||||
if TEST_MODE:
|
||||
# Retorno simulado
|
||||
return {
|
||||
"dry_run": True,
|
||||
"note": "TEST_MODE=True — retorno simulado.",
|
||||
@@ -366,7 +520,6 @@ def oci_chat_invoke(region: str, compartment_id: str, model_ocid: str, oci_paylo
|
||||
timeout=(10, 240)
|
||||
)
|
||||
|
||||
# Monta ChatDetails + GenericChatRequest com api_format=GENERIC
|
||||
chat_detail = oci.generative_ai_inference.models.ChatDetails()
|
||||
generic = oci.generative_ai_inference.models.GenericChatRequest()
|
||||
generic.api_format = oci.generative_ai_inference.models.BaseChatRequest.API_FORMAT_GENERIC
|
||||
@@ -378,9 +531,17 @@ def oci_chat_invoke(region: str, compartment_id: str, model_ocid: str, oci_paylo
|
||||
sdk_msg.role = m["role"]
|
||||
parts = []
|
||||
for c in m["content"]:
|
||||
tc = oci.generative_ai_inference.models.TextContent()
|
||||
tc.text = c.get("text", "")
|
||||
parts.append(tc)
|
||||
ctype = c.get("type")
|
||||
if ctype == "TEXT":
|
||||
tc = oci.generative_ai_inference.models.TextContent()
|
||||
tc.text = c.get("text", "")
|
||||
parts.append(tc)
|
||||
elif ctype == "IMAGE_URL":
|
||||
ic = oci.generative_ai_inference.models.ImageContent()
|
||||
iu = oci.generative_ai_inference.models.ImageUrl()
|
||||
iu.url = c.get("url", "")
|
||||
ic.image_url = iu
|
||||
parts.append(ic)
|
||||
sdk_msg.content = parts
|
||||
sdk_messages.append(sdk_msg)
|
||||
|
||||
@@ -396,13 +557,8 @@ def oci_chat_invoke(region: str, compartment_id: str, model_ocid: str, oci_paylo
|
||||
if "presence_penalty" in oci_payload:
|
||||
generic.presence_penalty = oci_payload["presence_penalty"]
|
||||
if "max_completion_tokens" in oci_payload:
|
||||
# Algumas versões do SDK usam 'max_tokens'; mantemos ambos por segurança
|
||||
generic.max_tokens = oci_payload["max_completion_tokens"]
|
||||
|
||||
# Extras (se suportados pelo modelo)
|
||||
# OBS: reasoning_effort/verbosity são específicos e podem não ter
|
||||
# mapeamento direto no SDK — ficam omitidos se não houver suporte.
|
||||
|
||||
chat_detail.serving_mode = oci.generative_ai_inference.models.OnDemandServingMode(model_id=model_ocid)
|
||||
chat_detail.chat_request = generic
|
||||
chat_detail.compartment_id = compartment_id
|
||||
@@ -410,16 +566,16 @@ def oci_chat_invoke(region: str, compartment_id: str, model_ocid: str, oci_paylo
|
||||
chat_response = client.chat(chat_detail)
|
||||
data = chat_response.data
|
||||
|
||||
# Normalize saída
|
||||
if hasattr(data, "chat_response") and data.chat_response and data.chat_response.choices:
|
||||
choice = data.chat_response.choices[0]
|
||||
# Tenta pegar o texto do primeiro bloco
|
||||
text = None
|
||||
if choice.message and choice.message.content:
|
||||
if hasattr(choice.message.content[0], "text"):
|
||||
text = choice.message.content[0].text
|
||||
# captura primeiro bloco de texto
|
||||
for block in choice.message.content:
|
||||
if hasattr(block, "text") and block.text:
|
||||
text = block.text
|
||||
break
|
||||
return {"output_text": text, "raw": "sdk"}
|
||||
# fallback
|
||||
return {"output_text": None, "raw": "unknown"}
|
||||
except Exception as e:
|
||||
return {"error": f"Falha ao chamar OCI: {e}"}
|
||||
@@ -484,24 +640,7 @@ def sse_chat_stream(model_label: str, full_text: str) -> Generator[str, None, No
|
||||
yield "data: [DONE]\n\n"
|
||||
|
||||
# ==========================
|
||||
# Segurança
|
||||
# ==========================
|
||||
|
||||
def check_api_key():
|
||||
expected_key = os.environ.get("API_KEY")
|
||||
if not expected_key:
|
||||
print("AVISO: API_KEY não configurada nas variáveis de ambiente.")
|
||||
return
|
||||
provided_key = request.headers.get("X-API-Key")
|
||||
if provided_key != expected_key:
|
||||
abort(401, description="Chave de API inválida ou ausente.")
|
||||
|
||||
@app.before_request
|
||||
def before_all_requests():
|
||||
check_api_key()
|
||||
|
||||
# ==========================
|
||||
# Endpoints existentes (mantidos)
|
||||
# Endpoints nativos
|
||||
# ==========================
|
||||
|
||||
@app.route("/", methods=["GET"])
|
||||
@@ -514,11 +653,7 @@ def var_copy(myvar):
|
||||
|
||||
@app.route("/genai-agent/<region>/<agent_endpoint_id>/session", methods=["POST"])
|
||||
def manage_session(region, agent_endpoint_id):
|
||||
"""
|
||||
Reaproveita ou cria uma sessão nova com base em channel + cuid.
|
||||
"""
|
||||
data = request.get_json() or {}
|
||||
# DEBUG:
|
||||
print(">>> /genai-agent/.../session payload recebido:")
|
||||
#print(data)
|
||||
channel = data.get("channel")
|
||||
@@ -531,7 +666,6 @@ def manage_session(region, agent_endpoint_id):
|
||||
@app.route("/genai-agent/<region>/<agent_endpoint_id>/<session_id>/chat", methods=["POST"])
|
||||
def agent_chat(region, agent_endpoint_id, session_id):
|
||||
data = request.get_json() or {}
|
||||
# DEBUG:
|
||||
print(">>> /genai-agent/.../chat payload recebido:")
|
||||
#print(data)
|
||||
user_message = data.get("userMessage")
|
||||
@@ -543,7 +677,6 @@ def agent_chat(region, agent_endpoint_id, session_id):
|
||||
@app.route("/genai/<region>/<compartment_id>/<model_id>/inference", methods=["POST"])
|
||||
def inference(region, compartment_id, model_id):
|
||||
data = request.get_json() or {}
|
||||
# DEBUG:
|
||||
print(">>> /inference request body:")
|
||||
#print(data)
|
||||
prompt = data.get("prompt")
|
||||
@@ -553,7 +686,7 @@ def inference(region, compartment_id, model_id):
|
||||
return jsonify(response_data)
|
||||
|
||||
# ==========================
|
||||
# Novos endpoints — OpenAI v1 compat
|
||||
# Endpoints OpenAI v1 compat — CHAT
|
||||
# ==========================
|
||||
|
||||
@app.route("/genai/<region>/<compartment_id>/<path_model_id>/v1/chat/completions", methods=["POST"])
|
||||
@@ -563,7 +696,6 @@ def v1_chat_completions(region, compartment_id, path_model_id):
|
||||
except Exception as e:
|
||||
return jsonify({"error": f"JSON inválido: {e}"}), 400
|
||||
|
||||
# DEBUG: imprimir o que chegou
|
||||
print(">>> /v1/chat/completions body recebido:")
|
||||
print(body)
|
||||
|
||||
@@ -577,6 +709,7 @@ def v1_chat_completions(region, compartment_id, path_model_id):
|
||||
if not isinstance(msgs, list) or not msgs:
|
||||
return jsonify({"error": "Campo 'messages' é obrigatório e deve ser uma lista."}), 400
|
||||
|
||||
# Suporte multimodal (text + image_url)
|
||||
oci_msgs = to_oci_messages(msgs)
|
||||
oci_payload = build_oci_chat_payload(oci_msgs, resolved["params"])
|
||||
|
||||
@@ -607,7 +740,6 @@ def v1_text_completions(region, compartment_id, path_model_id):
|
||||
except Exception as e:
|
||||
return jsonify({"error": f"JSON inválido: {e}"}), 400
|
||||
|
||||
# DEBUG:
|
||||
print(">>> /v1/completions body recebido:")
|
||||
print(body)
|
||||
|
||||
@@ -621,7 +753,7 @@ def v1_text_completions(region, compartment_id, path_model_id):
|
||||
if prompt is None:
|
||||
return jsonify({"error": "Campo 'prompt' é obrigatório."}), 400
|
||||
|
||||
# Compat: empacotar como chat com 1 mensagem user
|
||||
# Compat: empacotar como chat com 1 mensagem user (texto)
|
||||
if isinstance(prompt, list):
|
||||
prompt_text = "\n".join([str(p) for p in prompt])
|
||||
else:
|
||||
@@ -651,9 +783,146 @@ def v1_text_completions(region, compartment_id, path_model_id):
|
||||
|
||||
return jsonify(to_openai_text_response(model_label, output_text))
|
||||
|
||||
# ==========================
|
||||
# Endpoints OpenAI v1 — FILES
|
||||
# (com e sem prefixo /genai/<region>/<compartment_id>/<model>)
|
||||
# ==========================
|
||||
|
||||
def _files_upload_handler():
|
||||
# Compatível com multipart/form-data (OpenAI clients)
|
||||
if "file" not in request.files:
|
||||
return jsonify({"error": "Campo 'file' é obrigatório"}), 400
|
||||
f = request.files["file"]
|
||||
result = upload_file_to_bucket(f, f.filename)
|
||||
return jsonify(result)
|
||||
|
||||
def _files_list_handler():
|
||||
if TEST_MODE:
|
||||
# Lista fictícia em modo teste
|
||||
return jsonify({"data": [
|
||||
{"id": fid, "object": "file", "filename": os.path.basename(obj), "bytes": 0}
|
||||
for fid, obj in FILE_INDEX.items()
|
||||
]})
|
||||
resp = object_client.list_objects(namespace, BUCKET_NAME, prefix=UPLOAD_PREFIX)
|
||||
files = []
|
||||
for obj in resp.data.objects:
|
||||
files.append({
|
||||
"id": f"file-{uuid.uuid4().hex[:12]}",
|
||||
"object": "file",
|
||||
"filename": obj.name.replace(UPLOAD_PREFIX, ""),
|
||||
"bytes": obj.size
|
||||
})
|
||||
return jsonify({"data": files})
|
||||
|
||||
# Sem prefixo
|
||||
#@app.route("/v1/files", methods=["POST"])
|
||||
#@app.route("/genai/v1/files", methods=["POST"])
|
||||
@app.route("/genai/<region>/<compartment_id>/<path_model_id>/v1/files", methods=["POST"])
|
||||
def v1_files_upload(region=None, compartment_id=None, path_model_id=None):
|
||||
return _files_upload_handler()
|
||||
|
||||
#@app.route("/v1/files", methods=["GET"])
|
||||
#@app.route("/genai/v1/files", methods=["GET"])
|
||||
@app.route("/genai/<region>/<compartment_id>/<path_model_id>/v1/files", methods=["GET"])
|
||||
def v1_files_list(region=None, compartment_id=None, path_model_id=None):
|
||||
return _files_list_handler()
|
||||
|
||||
#@app.route("/v1/files/<file_id>/content", methods=["GET"])
|
||||
#@app.route("/genai/v1/files/<file_id>/content", methods=["GET"])
|
||||
@app.route("/genai/<region>/<compartment_id>/<path_model_id>/v1/files/<file_id>/content", methods=["GET"])
|
||||
def v1_files_content(file_id, region=None, compartment_id=None, path_model_id=None):
|
||||
"""
|
||||
Fallback para servir o conteúdo via Flask (caso cliente não use a signed URL).
|
||||
"""
|
||||
if TEST_MODE:
|
||||
return jsonify({"note": "TEST_MODE — conteúdo não disponível"}), 200
|
||||
obj = FILE_INDEX.get(file_id)
|
||||
if not obj:
|
||||
return jsonify({"error": "file_id não encontrado neste servidor"}), 404
|
||||
# stream direto do Object Storage
|
||||
obj_resp = object_client.get_object(namespace, BUCKET_NAME, obj)
|
||||
data = obj_resp.data.content
|
||||
# tentativa de inferir mime pelo nome armazenado
|
||||
filename = os.path.basename(obj)
|
||||
return send_file(
|
||||
io.BytesIO(data.read()),
|
||||
mimetype=guess_mime(filename, "application/octet-stream"),
|
||||
as_attachment=False,
|
||||
download_name=filename
|
||||
)
|
||||
|
||||
# ==========================
|
||||
# Endpoints OpenAI v1 — IMAGES (gera/edita/varia) com retorno via PAR
|
||||
# ==========================
|
||||
|
||||
def _store_image_bytes_and_return_url(image_bytes: bytes, filename: str) -> str:
|
||||
"""
|
||||
Armazena bytes no bucket e retorna signed URL.
|
||||
"""
|
||||
if TEST_MODE:
|
||||
return f"https://objectstorage.{region}.oraclecloud.com/test/{UPLOAD_PREFIX}{uuid.uuid4().hex}_{filename}"
|
||||
object_name = f"{UPLOAD_PREFIX}{uuid.uuid4().hex}_{filename}"
|
||||
object_client.put_object(
|
||||
namespace, BUCKET_NAME, object_name, image_bytes, content_type=guess_mime(filename, "image/png")
|
||||
)
|
||||
return create_par_for_object(object_name, hours_valid=24)
|
||||
|
||||
#@app.route("/v1/images/generations", methods=["POST"])
|
||||
#@app.route("/genai/v1/images/generations", methods=["POST"])
|
||||
@app.route("/genai/<region>/<compartment_id>/<path_model_id>/v1/images/generations", methods=["POST"])
|
||||
def v1_images_generations(region=None, compartment_id=None, path_model_id=None):
|
||||
"""
|
||||
Geração de imagens a partir de prompt — placeholder.
|
||||
Integração com serviço de geração de imagens pode ser plugada aqui.
|
||||
Por ora, apenas armazena um 'mock' (PNG vazio) e retorna URL.
|
||||
"""
|
||||
body = request.form or request.get_json(force=True, silent=True) or {}
|
||||
prompt = body.get("prompt")
|
||||
if not prompt:
|
||||
return jsonify({"error": "Campo 'prompt' é obrigatório"}), 400
|
||||
|
||||
# MOCK: cria PNG vazio (1x1) — substitua por integração real de geração.
|
||||
png_bytes = base64.b64decode(
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHuwKp9w8H2AAAAABJRU5ErkJggg=="
|
||||
)
|
||||
url = _store_image_bytes_and_return_url(png_bytes, "generation.png")
|
||||
return jsonify({"created": int(time.time()), "data": [{"url": url}]})
|
||||
|
||||
#@app.route("/v1/images/edits", methods=["POST"])
|
||||
#@app.route("/genai/v1/images/edits", methods=["POST"])
|
||||
@app.route("/genai/<region>/<compartment_id>/<path_model_id>/v1/images/edits", methods=["POST"])
|
||||
def v1_images_edits(region=None, compartment_id=None, path_model_id=None):
|
||||
"""
|
||||
Edição de imagem — placeholder.
|
||||
Espera multipart com 'image' (arquivo base) e 'prompt'.
|
||||
"""
|
||||
if "image" not in request.files:
|
||||
return jsonify({"error": "Campo 'image' (multipart) é obrigatório"}), 400
|
||||
prompt = request.form.get("prompt", "")
|
||||
base_img = request.files["image"].read()
|
||||
# MOCK: retorna a própria imagem (sem edição)
|
||||
url = _store_image_bytes_and_return_url(base_img, "edit.png")
|
||||
return jsonify({"created": int(time.time()), "data": [{"url": url, "note": "mock edit"}]})
|
||||
|
||||
#@app.route("/v1/images/variations", methods=["POST"])
|
||||
#@app.route("/genai/v1/images/variations", methods=["POST"])
|
||||
@app.route("/genai/<region>/<compartment_id>/<path_model_id>/v1/images/variations", methods=["POST"])
|
||||
def v1_images_variations(region=None, compartment_id=None, path_model_id=None):
|
||||
"""
|
||||
Variações de imagem — placeholder.
|
||||
Espera multipart com 'image' (arquivo base).
|
||||
"""
|
||||
if "image" not in request.files:
|
||||
return jsonify({"error": "Campo 'image' (multipart) é obrigatório"}), 400
|
||||
base_img = request.files["image"].read()
|
||||
# MOCK: retorna a própria imagem (sem variação)
|
||||
url = _store_image_bytes_and_return_url(base_img, "variation.png")
|
||||
return jsonify({"created": int(time.time()), "data": [{"url": url, "note": "mock variation"}]})
|
||||
|
||||
# ==========================
|
||||
# Main
|
||||
# ==========================
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Observação: para produção, use um servidor WSGI (gunicorn/uwsgi) atrás de um proxy.
|
||||
app.run(host='0.0.0.0', port=8000)
|
||||
|
||||
Reference in New Issue
Block a user