Compare commits
2 Commits
402d44111e
...
a91b20adc6
| Author | SHA1 | Date | |
|---|---|---|---|
| a91b20adc6 | |||
| 811608911a |
32
analyzer.py
32
analyzer.py
@@ -4,19 +4,29 @@ from google import genai
|
|||||||
from google.genai import types
|
from google.genai import types
|
||||||
|
|
||||||
SYSTEM_PROMPT_RU = (
|
SYSTEM_PROMPT_RU = (
|
||||||
"Ты анализируешь кадры с Twitch-стрима. "
|
"Ты анализируешь кадры с Twitch-стрима. Дай подробное описание всего, что видишь на экране:\n"
|
||||||
"Кратко опиши что происходит на экране: игра, действия стримера, "
|
"1. Какая игра/приложение на экране, жанр, сеттинг\n"
|
||||||
"интерфейс, чат, оверлеи. Будь лаконичен (2-3 предложения). "
|
"2. Что конкретно происходит: действия персонажа, ситуация в игре, этап (меню, геймплей, катсцена, лобби)\n"
|
||||||
"Если ничего не изменилось по сравнению с предыдущим описанием, "
|
"3. Элементы интерфейса: HUD, здоровье, инвентарь, мини-карта, счёт, таймеры\n"
|
||||||
"скажи 'Без изменений' и уточни только новые детали."
|
"4. Камера стримера: что видно, эмоции, жесты (если есть)\n"
|
||||||
|
"5. Оверлеи: донаты, алерты, виджеты, чат\n"
|
||||||
|
"6. Текст на экране: любой читаемый текст, названия, никнеймы\n"
|
||||||
|
"Пиши развёрнуто (5-10 предложений). Описание должно быть достаточно детальным, "
|
||||||
|
"чтобы другая AI-модель могла полностью понять контекст происходящего без просмотра изображения.\n"
|
||||||
|
"Если сцена похожа на предыдущую, опиши только изменения, но подробно."
|
||||||
)
|
)
|
||||||
|
|
||||||
SYSTEM_PROMPT_EN = (
|
SYSTEM_PROMPT_EN = (
|
||||||
"You are analyzing frames from a Twitch stream. "
|
"You are analyzing frames from a Twitch stream. Give a detailed description of everything on screen:\n"
|
||||||
"Briefly describe what's happening on screen: game, streamer actions, "
|
"1. What game/application is shown, genre, setting\n"
|
||||||
"UI, chat, overlays. Be concise (2-3 sentences). "
|
"2. What exactly is happening: character actions, game situation, stage (menu, gameplay, cutscene, lobby)\n"
|
||||||
"If nothing changed compared to the previous description, "
|
"3. UI elements: HUD, health, inventory, minimap, score, timers\n"
|
||||||
"say 'No changes' and only note new details."
|
"4. Streamer camera: what's visible, emotions, gestures (if present)\n"
|
||||||
|
"5. Overlays: donations, alerts, widgets, chat\n"
|
||||||
|
"6. On-screen text: any readable text, names, nicknames\n"
|
||||||
|
"Write in detail (5-10 sentences). The description must be detailed enough "
|
||||||
|
"for another AI model to fully understand the context without seeing the image.\n"
|
||||||
|
"If the scene is similar to the previous one, describe only the changes, but in detail."
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -67,7 +77,7 @@ class VisionAnalyzer:
|
|||||||
contents=contents,
|
contents=contents,
|
||||||
config=types.GenerateContentConfig(
|
config=types.GenerateContentConfig(
|
||||||
system_instruction=self.system_prompt,
|
system_instruction=self.system_prompt,
|
||||||
max_output_tokens=300,
|
max_output_tokens=1000,
|
||||||
temperature=0.3,
|
temperature=0.3,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|||||||
41
capture.py
41
capture.py
@@ -1,6 +1,10 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
from collections.abc import AsyncIterator
|
from collections.abc import AsyncIterator
|
||||||
|
|
||||||
|
from rich.console import Console
|
||||||
|
|
||||||
|
console = Console()
|
||||||
|
|
||||||
|
|
||||||
async def _pipe_stream(source: asyncio.StreamReader, dest: asyncio.StreamWriter):
|
async def _pipe_stream(source: asyncio.StreamReader, dest: asyncio.StreamWriter):
|
||||||
"""Forward data from streamlink stdout to ffmpeg stdin."""
|
"""Forward data from streamlink stdout to ffmpeg stdin."""
|
||||||
@@ -17,6 +21,17 @@ async def _pipe_stream(source: asyncio.StreamReader, dest: asyncio.StreamWriter)
|
|||||||
dest.close()
|
dest.close()
|
||||||
|
|
||||||
|
|
||||||
|
async def _log_stderr(proc_name: str, stderr: asyncio.StreamReader):
|
||||||
|
"""Read and display stderr from a subprocess."""
|
||||||
|
while True:
|
||||||
|
line = await stderr.readline()
|
||||||
|
if not line:
|
||||||
|
break
|
||||||
|
text = line.decode("utf-8", errors="replace").rstrip()
|
||||||
|
if text:
|
||||||
|
console.print(f"[dim red][{proc_name}] {text}[/dim red]")
|
||||||
|
|
||||||
|
|
||||||
async def capture_frames(
|
async def capture_frames(
|
||||||
channel: str, quality: str, interval: int
|
channel: str, quality: str, interval: int
|
||||||
) -> AsyncIterator[bytes]:
|
) -> AsyncIterator[bytes]:
|
||||||
@@ -33,6 +48,7 @@ async def capture_frames(
|
|||||||
|
|
||||||
ffmpeg_cmd = [
|
ffmpeg_cmd = [
|
||||||
"ffmpeg",
|
"ffmpeg",
|
||||||
|
"-loglevel", "warning",
|
||||||
"-i", "pipe:0",
|
"-i", "pipe:0",
|
||||||
"-vf", f"fps=1/{interval}",
|
"-vf", f"fps=1/{interval}",
|
||||||
"-f", "image2pipe",
|
"-f", "image2pipe",
|
||||||
@@ -41,24 +57,43 @@ async def capture_frames(
|
|||||||
"pipe:1",
|
"pipe:1",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
console.print("[dim]Starting streamlink...[/dim]")
|
||||||
streamlink_proc = await asyncio.create_subprocess_exec(
|
streamlink_proc = await asyncio.create_subprocess_exec(
|
||||||
*streamlink_cmd,
|
*streamlink_cmd,
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stderr=asyncio.subprocess.DEVNULL,
|
stderr=asyncio.subprocess.PIPE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Wait a moment and check if streamlink started OK
|
||||||
|
await asyncio.sleep(2)
|
||||||
|
if streamlink_proc.returncode is not None:
|
||||||
|
stderr_out = await streamlink_proc.stderr.read()
|
||||||
|
raise RuntimeError(
|
||||||
|
f"streamlink exited with code {streamlink_proc.returncode}: "
|
||||||
|
f"{stderr_out.decode('utf-8', errors='replace')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
console.print("[dim]Starting ffmpeg...[/dim]")
|
||||||
ffmpeg_proc = await asyncio.create_subprocess_exec(
|
ffmpeg_proc = await asyncio.create_subprocess_exec(
|
||||||
*ffmpeg_cmd,
|
*ffmpeg_cmd,
|
||||||
stdin=asyncio.subprocess.PIPE,
|
stdin=asyncio.subprocess.PIPE,
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdout=asyncio.subprocess.PIPE,
|
||||||
stderr=asyncio.subprocess.DEVNULL,
|
stderr=asyncio.subprocess.PIPE,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Log stderr from both processes
|
||||||
|
stderr_tasks = [
|
||||||
|
asyncio.create_task(_log_stderr("streamlink", streamlink_proc.stderr)),
|
||||||
|
asyncio.create_task(_log_stderr("ffmpeg", ffmpeg_proc.stderr)),
|
||||||
|
]
|
||||||
|
|
||||||
# Forward streamlink → ffmpeg in background
|
# Forward streamlink → ffmpeg in background
|
||||||
pipe_task = asyncio.create_task(
|
pipe_task = asyncio.create_task(
|
||||||
_pipe_stream(streamlink_proc.stdout, ffmpeg_proc.stdin)
|
_pipe_stream(streamlink_proc.stdout, ffmpeg_proc.stdin)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
console.print("[dim]Pipeline running, waiting for first frame...[/dim]")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
buf = b""
|
buf = b""
|
||||||
while True:
|
while True:
|
||||||
@@ -82,6 +117,8 @@ async def capture_frames(
|
|||||||
yield frame
|
yield frame
|
||||||
finally:
|
finally:
|
||||||
pipe_task.cancel()
|
pipe_task.cancel()
|
||||||
|
for t in stderr_tasks:
|
||||||
|
t.cancel()
|
||||||
for proc in (ffmpeg_proc, streamlink_proc):
|
for proc in (ffmpeg_proc, streamlink_proc):
|
||||||
try:
|
try:
|
||||||
proc.terminate()
|
proc.terminate()
|
||||||
|
|||||||
30
main.py
30
main.py
@@ -28,20 +28,26 @@ async def run(config) -> None:
|
|||||||
|
|
||||||
frame_number = 0
|
frame_number = 0
|
||||||
|
|
||||||
async for frame_data in capture_frames(
|
try:
|
||||||
config.channel, config.quality, config.interval
|
async for frame_data in capture_frames(
|
||||||
):
|
config.channel, config.quality, config.interval
|
||||||
frame_number += 1
|
):
|
||||||
console.print(f"[dim]Captured frame #{frame_number}, analyzing...[/dim]")
|
frame_number += 1
|
||||||
|
console.print(f"[dim]Captured frame #{frame_number}, analyzing...[/dim]")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
description = await analyzer.analyze_frame(frame_data)
|
description = await analyzer.analyze_frame(frame_data)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
console.print(f"[bold red]Analysis error:[/bold red] {e}")
|
console.print(f"[bold red]Analysis error:[/bold red] {e}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
print_description(description, frame_number)
|
print_description(description, frame_number)
|
||||||
await log_description(config.log_file, description, frame_number)
|
await log_description(config.log_file, description, frame_number)
|
||||||
|
except RuntimeError as e:
|
||||||
|
console.print(f"[bold red]Error:[/bold red] {e}")
|
||||||
|
finally:
|
||||||
|
if frame_number == 0:
|
||||||
|
console.print("[bold yellow]No frames were captured.[/bold yellow]")
|
||||||
|
|
||||||
|
|
||||||
def main() -> None:
|
def main() -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user