Spaces:
Running
Running
import json | |
import httpx | |
import asyncio | |
from typing import Dict, Any, Optional | |
from utils.logger import SpotMakerLogger | |
class ShootingBoardGenerator: | |
""" | |
Generator szczeg贸艂owego scenariusza spotu reklamowego przy u偶yciu Hailuo API. | |
""" | |
def __init__(self, api_key: str, logger: SpotMakerLogger): | |
self.api_key = api_key | |
self.logger = logger | |
self.api_url = "https://api.minimaxi.chat/v1/text/chatcompletion_v2" | |
self.model = "MiniMax-Text-01" | |
self.max_retries = 3 | |
self.retry_delay = 5 # sekundy | |
async def generate(self, concept: str) -> Dict[str, Any]: | |
""" | |
Generuje szczeg贸艂owy scenariusz na podstawie koncepcji. | |
Args: | |
concept: Opis koncepcji reklamowej | |
Returns: | |
Dict zawieraj膮cy scenariusz i tekst lektora | |
""" | |
retry_count = 0 | |
last_error = None | |
while retry_count < self.max_retries: | |
try: | |
self.logger.log("shooting_board", | |
f"Pr贸ba {retry_count + 1}/{self.max_retries}", | |
"info") | |
self.logger.update_progress("shooting_board", 10, "Przygotowuj臋 prompt...") | |
prompt = self._prepare_prompt(concept) | |
self.logger.update_progress("shooting_board", 30, "Wysy艂am zapytanie do API...") | |
response = await self._call_api(prompt) | |
self.logger.update_progress("shooting_board", 60, "Przetwarzam odpowied藕...") | |
result = self._parse_response(response) | |
self.logger.update_progress("shooting_board", 100, "Zako艅czono generowanie scenariusza") | |
return result | |
except httpx.TimeoutException as e: | |
last_error = f"Timeout podczas po艂膮czenia z API: {str(e)}" | |
self.logger.log("shooting_board", last_error, "warning") | |
except httpx.HTTPError as e: | |
last_error = f"B艂膮d HTTP podczas po艂膮czenia z API: {str(e)}" | |
self.logger.log("shooting_board", last_error, "warning") | |
except Exception as e: | |
if "Znaleziono" in str(e) and "uj臋膰" in str(e): | |
# B艂膮d parsowania odpowiedzi - nie pr贸bujemy ponownie | |
raise | |
last_error = str(e) | |
self.logger.log("shooting_board", f"Nieoczekiwany b艂膮d: {last_error}", "warning") | |
retry_count += 1 | |
if retry_count < self.max_retries: | |
delay = self.retry_delay * retry_count | |
self.logger.log("shooting_board", | |
f"Ponawiam za {delay}s...", | |
"info") | |
await asyncio.sleep(delay) | |
# Je艣li wszystkie pr贸by si臋 nie powiod艂y | |
error_msg = f"Nie uda艂o si臋 wygenerowa膰 scenariusza po {self.max_retries} pr贸bach. Ostatni b艂膮d: {last_error}" | |
self.logger.log("shooting_board", error_msg, "error") | |
raise Exception(error_msg) | |
def _prepare_prompt(self, concept: str) -> str: | |
"""Przygotowuje prompt dla modelu LLM.""" | |
return f"""Jako do艣wiadczony re偶yser reklam, stw贸rz szczeg贸艂owy scenariusz spotu reklamowego | |
na podstawie poni偶szej koncepcji. | |
Koncepcja: | |
{concept} | |
Utw贸rz DOK艁ADNIE 5 uj臋膰 oraz tekst lektora. Format odpowiedzi MUSI by膰 nast臋puj膮cy: | |
Uj臋cie 1: [szczeg贸艂owy opis pierwszej sceny] | |
Uj臋cie 2: [szczeg贸艂owy opis drugiej sceny] | |
Uj臋cie 3: [szczeg贸艂owy opis trzeciej sceny] | |
Uj臋cie 4: [szczeg贸艂owy opis czwartej sceny] | |
Uj臋cie 5: [szczeg贸艂owy opis pi膮tej sceny] | |
TEKST LEKTORA: | |
[tekst lektora] | |
UWAGA: Odpowied藕 MUSI zawiera膰 DOK艁ADNIE 5 uj臋膰 oznaczonych numerami od 1 do 5. | |
Ka偶de uj臋cie musi zaczyna膰 si臋 od "Uj臋cie X:" gdzie X to numer uj臋cia. | |
Tekst lektora musi by膰 oddzielony nag艂贸wkiem "TEKST LEKTORA:". | |
Prosz臋 艣ci艣le trzyma膰 si臋 tego formatu.""" | |
async def _call_api(self, prompt: str) -> Dict[str, Any]: | |
"""Wysy艂a zapytanie do Hailuo API.""" | |
headers = { | |
"Content-Type": "application/json", | |
"Authorization": f"Bearer {self.api_key}" | |
} | |
data = { | |
"model": self.model, | |
"temperature": 0.2, # Ni偶sza temperatura dla bardziej sp贸jnych wynik贸w | |
"max_tokens": 1000, # Zwi臋kszamy limit token贸w dla d艂u偶szych opis贸w | |
"messages": [ | |
{ | |
"role": "system", | |
"name": "Director", | |
"content": "Jeste艣 do艣wiadczonym re偶yserem reklam specjalizuj膮cym si臋 w tworzeniu kreatywnych i anga偶uj膮cych spot贸w reklamowych." | |
}, | |
{ | |
"role": "user", | |
"name": "user", | |
"content": prompt | |
} | |
] | |
} | |
self.logger.log("shooting_board", | |
f"Wysy艂am request do: {self.api_url}", | |
"debug") | |
async with httpx.AsyncClient() as client: | |
try: | |
response = await client.post( | |
self.api_url, | |
headers=headers, | |
json=data, | |
timeout=30.0 | |
) | |
if response.status_code != 200: | |
error_msg = f"API error: {response.status_code} - {response.text}" | |
self.logger.log("shooting_board", error_msg, "error") | |
raise httpx.HTTPError(error_msg) | |
return response.json() | |
except httpx.TimeoutException as e: | |
raise httpx.TimeoutException(f"Timeout podczas po艂膮czenia z API: {str(e)}") | |
except Exception as e: | |
raise Exception(f"B艂膮d podczas wywo艂ania API: {str(e)}") | |
def _parse_response(self, response: Dict[str, Any]) -> Dict[str, Any]: | |
"""Przetwarza odpowied藕 z API.""" | |
try: | |
if response.get('base_resp', {}).get('status_code') != 0: | |
error_msg = response.get('base_resp', {}).get('status_msg', 'Unknown API error') | |
raise ValueError(f"API error: {error_msg}") | |
content = response['choices'][0]['message']['content'] | |
self.logger.log("shooting_board", f"Przetwarzam odpowied藕:\n{content}", "debug") | |
# Przygotowanie kontener贸w na wyniki | |
key_frames = [] | |
voice_over = "" | |
# Podziel tekst na linie i znajd藕 uj臋cia oraz tekst lektora | |
lines = content.split('\n') | |
current_section = None | |
current_description = "" | |
for line in lines: | |
line = line.strip() | |
if not line: | |
continue | |
# Bardziej elastyczne wykrywanie uj臋膰 | |
if any(marker in line.lower() for marker in ["uj臋cie", "scena", "frame", "shot"]): | |
# Je艣li mamy poprzedni opis, zapisz go | |
if current_description and current_section == 'frame': | |
key_frames.append({"description": current_description.strip()}) | |
current_section = 'frame' | |
current_description = line.split(":", 1)[1].strip() if ":" in line else "" | |
elif any(marker in line.upper() for marker in ["TEKST LEKTORA", "VOICE OVER", "LEKTOR"]): | |
# Je艣li mamy ostatni opis uj臋cia, zapisz go | |
if current_description and current_section == 'frame': | |
key_frames.append({"description": current_description.strip()}) | |
current_section = 'lektor' | |
current_description = "" | |
else: | |
# Dodaj lini臋 do aktualnego opisu | |
if current_section == 'frame': | |
current_description += " " + line | |
elif current_section == 'lektor': | |
voice_over += " " + line.strip('"') | |
# Dodaj ostatni opis je艣li istnieje | |
if current_description and current_section == 'frame': | |
key_frames.append({"description": current_description.strip()}) | |
# Walidacja wynik贸w z bardziej szczeg贸艂owym komunikatem b艂臋du | |
if len(key_frames) != 5: | |
error_details = f"Znaleziono {len(key_frames)} uj臋膰 zamiast wymaganych 5." | |
self.logger.log("shooting_board", error_details, "error") | |
self.logger.log("shooting_board", f"Pe艂na odpowied藕: {content}", "debug") | |
raise ValueError(error_details) | |
if not voice_over.strip(): | |
raise ValueError("Brak tekstu dla lektora") | |
return { | |
"key_frames": key_frames, | |
"voice_over": voice_over.strip() | |
} | |
except Exception as e: | |
error_msg = f"B艂膮d przetwarzania odpowiedzi: {str(e)}" | |
self.logger.log("shooting_board", error_msg, "error") | |
raise Exception(error_msg) | |
def get_status(self) -> Dict[str, Any]: | |
"""Zwraca aktualny status generatora.""" | |
return { | |
'progress': self.logger.get_module_progress("shooting_board"), | |
'status': self.logger.get_module_status("shooting_board") | |
} |