File size: 7,070 Bytes
e83b61c
4dc8140
 
e83b61c
 
 
 
4dc8140
e83b61c
 
4dc8140
e83b61c
4dc8140
e83b61c
 
4dc8140
 
 
e83b61c
4dc8140
 
e83b61c
4dc8140
e83b61c
 
 
 
 
4dc8140
e83b61c
4dc8140
e83b61c
 
 
 
4dc8140
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e83b61c
4dc8140
 
 
e83b61c
4dc8140
e83b61c
 
4dc8140
e83b61c
4dc8140
 
 
 
 
e83b61c
 
4dc8140
e83b61c
4dc8140
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e83b61c
4dc8140
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e83b61c
 
4dc8140
 
 
 
e83b61c
4dc8140
 
e83b61c
 
4dc8140
 
 
e83b61c
4dc8140
 
 
e83b61c
 
4dc8140
e83b61c
4dc8140
 
 
e83b61c
4dc8140
e83b61c
4dc8140
 
e83b61c
4dc8140
e83b61c
 
4dc8140
e83b61c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
import os
from typing import Generator, List, Tuple

import gradio as gr
from gradio_client import Client, handle_file
from openai import OpenAI

# --- Конфигурация (в HF Spaces добавь NV_API_KEY в Secrets) ---
NV_API_KEY = os.environ.get("NV_API_KEY")
if not NV_API_KEY:
    raise RuntimeError("Добавьте NV_API_KEY в Secrets Hugging Face Space")

# Florence-2 (публичный wrapper)
florence = Client("gokaygokay/Florence-2")

# OpenAI-compatible client (NVIDIA integrate)
llm = OpenAI(base_url="https://integrate.api.nvidia.com/v1", api_key=NV_API_KEY)


def get_caption(image_path: str) -> str:
    """Запрос 'More Detailed Caption' к Florence-2. image_path может быть URL или локальный путь."""
    try:
        # handle_file поддерживает URL и локальные файлы
        result = florence.predict(
            image=handle_file(image_path),
            task_prompt="More Detailed Caption",
            text_input=None,
            model_id="microsoft/Florence-2-large",
            api_name="/process_image",
        )
        # result может быть строкой или структурой — нормализуем
        return result if isinstance(result, str) else str(result)
    except Exception as e:
        return f"[Ошибка при генерации подписи: {e}]"


def _extract_text_from_chunk(chunk) -> str:
    """Универсальная попытка извлечь текстовый фрагмент из stream-chunk."""
    try:
        # объект-атрибутный стиль
        if hasattr(chunk, "choices"):
            choice = chunk.choices[0]
            delta = getattr(choice, "delta", None)
            if delta is not None:
                txt = getattr(delta, "content", None) or getattr(delta, "reasoning_content", None)
                return txt or ""
        # dict-стиль
        if isinstance(chunk, dict):
            choices = chunk.get("choices", [])
            if choices:
                delta = choices[0].get("delta", {})
                return delta.get("content") or delta.get("reasoning_content") or ""
    except Exception:
        return ""
    return ""


def chat_stream(image_path: str, user_message: str, history: List[Tuple[str, str]]):
    """
    Generator для Gradio: сначала возвращает caption, затем по мере прихода токенов
    обновляет последний ответ ассистента.
    Возвращаемые значения — кортежи (history, caption) соответствующие outputs.
    """
    history = history or []

    if not image_path:
        history.append([user_message, "Пожалуйста, загрузите изображение."])
        yield history, ""
        return

    # Получаем подробную подпись
    caption = get_caption(image_path)

    # Сборка системного промпта
    system_prompt = (
        "You are 'multimodal gpt-oss 120b'. Use the provided 'More Detailed Caption' as authoritative visual context.\n\n"
        "Image Caption START >>>\n"
        f"{caption}\n"
        "<<< Image Caption END.\n"
        "When answering, mention visible details and be explicit when uncertain."
    )

    # Добавляем сообщение пользователя
    history.append([user_message, ""])
    # Первый yield — чтобы UI сразу показал пользовательское сообщение и подпись
    yield history, caption

    assistant_text = ""
    try:
        stream = llm.chat.completions.create(
            model="openai/gpt-oss-120b",
            messages=[
                {"role": "system", "content": system_prompt},
                {"role": "user", "content": user_message},
            ],
            temperature=0.8,
            top_p=1.0,
            max_tokens=1024,
            stream=True,
        )

        for chunk in stream:
            piece = _extract_text_from_chunk(chunk)
            if not piece:
                continue
            assistant_text += piece
            history[-1][1] = assistant_text
            yield history, caption

    except Exception as e:
        # В случае ошибки — покажем её в чате
        history[-1][1] = f"[Ошибка стриминга LLM: {e}]"
        yield history, caption

    # Финальный yield (гарантируем состояние завершения)
    yield history, caption


# --- UI (для HF Spaces) ---
EXAMPLE_IMAGES = [
    # список простых строк (URL или локальные пути). НИКАКИХ вложенных списков!
    "https://raw.githubusercontent.com/gradio-app/gradio/main/test/test_files/bus.png",
    "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cats.png",
    "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/cheetah.jpg",
    "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/flowers.png",
]

css = """
#title {text-align:center; margin-bottom: -18px;}
.gradio-container { max-width: 1100px; margin: auto; }
"""

with gr.Blocks(theme=gr.themes.Soft(), css=css) as demo:
    gr.Markdown("<h2 id='title'>🖼️ multimodal gpt-oss 120b — визуальный чат</h2>")
    with gr.Row():
        with gr.Column(scale=4):
            image_input = gr.Image(label="Загрузите картинку или выберите из галереи", type="filepath", tool="editor")
            raw_caption = gr.Textbox(label="More Detailed Caption (Florence-2)", interactive=False)
            user_input = gr.Textbox(label="Вопрос по изображению", placeholder="Например: 'Что происходит на фото?'")
            send_btn = gr.Button("Отправить")
            clear_btn = gr.Button("Очистить чат")
            gr.Markdown("**Галерея примеров (клик — подставить в загрузчик)**")
            gallery = gr.Gallery(value=EXAMPLE_IMAGES, columns=4, label="Примеры", show_label=False).style(grid=[4], height="auto")

        with gr.Column(scale=6):
            chatbot = gr.Chatbot(label="Чат с моделью", height=600)

    # Клик по картинке в галерее -> вставляем URL/путь в image_input
    def pick_example(img_url: str):
        return img_url

    gallery.select(fn=pick_example, inputs=[gallery], outputs=[image_input])

    # Кнопка отправки: привязываем генератор, который возвращает (chat_history, caption)
    send_btn.click(fn=chat_stream, inputs=[image_input, user_input, chatbot], outputs=[chatbot, raw_caption])

    clear_btn.click(lambda: [], None, chatbot)

if __name__ == "__main__":
    demo.launch(server_name="0.0.0.0", server_port=int(os.environ.get("PORT", 7860)))