Bundle Blue Archive roster, enable ffmpeg cog, refresh README
This commit is contained in:
1 parent
c815ea4449
commit
23b8c3722a
5 files changed
+176
-14
No files matched your search
@@ -14,7 +14,6 @@ icy_metadata_radios.md
|
|||||||
__pycache__/
|
__pycache__/
|
||||||
ssh_update.py
|
ssh_update.py
|
||||||
ssh_*.py
|
ssh_*.py
|
||||||
blue_archive/ba_students.json
|
|
||||||
_get_id.py
|
_get_id.py
|
||||||
AGENTS.md
|
AGENTS.md
|
||||||
firebase-service-account.json
|
firebase-service-account.json
|
||||||
|
|||||||
@@ -5,9 +5,9 @@ it started as a simple music bot and became a massive mess of features i actuall
|
|||||||
|
|
||||||
## features
|
## features
|
||||||
|
|
||||||
- **ai** — uses deepseek v4 (flash/pro). full vision support (well... vision is a little broken rn), continuous memory summarization (so it doesn't forget context), per-channel personas (yuuka/rem), and native deepseek-reasoner thinking toggles. doesn't break character.
|
- **ai** — uses deepseek v4 (flash/pro). text-only (no image/vision input), continuous memory summarization (so it doesn't forget context), per-channel personas (yuuka/rem), and native deepseek-reasoner thinking toggles. doesn't break character.
|
||||||
- **bridges** — two-way telegram <-> discord message forwarding. also pulls twitter/x leaks via rss and dumps them into a channel.d
|
- **bridges** — two-way telegram <-> discord message forwarding. twitter/x leaks via rss is implemented but currently disabled (Nitter RSS instances are dead).
|
||||||
- **media & music** — yt-dlp + ffmpeg based music playback. also has commands for random ffmpeg media conversions.
|
- **media & music** — lavalink + wavelink music playback (youtube search + http/icecast radio streams). also has ffmpeg media conversion commands (!togif, !caption, !reverse, !speed, !tomp4, !tomp3, !toopus).
|
||||||
- **sysinfo** — live monitoring of my laptop's cpu, ram, temps (lm-sensors), top processes, and fastfetch.
|
- **sysinfo** — live monitoring of my laptop's cpu, ram, temps (lm-sensors), top processes, and fastfetch.
|
||||||
- **utilities** — live currency conversion, steam stat tracking (strinova player counts).
|
- **utilities** — live currency conversion, steam stat tracking (strinova player counts).
|
||||||
- **calendar** — firestore-backed event booking via modal. autoconverts local times to UTC using a country list, feeds a web calendar frontend.
|
- **calendar** — firestore-backed event booking via modal. autoconverts local times to UTC using a country list, feeds a web calendar frontend.
|
||||||
@@ -19,10 +19,9 @@ it started as a simple music bot and became a massive mess of features i actuall
|
|||||||
|
|
||||||
- python + `discord.py`
|
- python + `discord.py`
|
||||||
- deepseek api for the ai stuff
|
- deepseek api for the ai stuff
|
||||||
- yt-dlp + ffmpeg for audio
|
- wavelink + lavalink (java) for music
|
||||||
- psutil + lm-sensors for hardware stats
|
- psutil + lm-sensors for hardware stats
|
||||||
- hosted on an acer nitro AN515-51 running arch linux
|
- runs as systemd services: `discordbot` and `lavalink`
|
||||||
- runs as a systemd service, updated via git pull over ssh
|
|
||||||
|
|
||||||
## commands
|
## commands
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
[{"Id": 10000, "IsReleased": [true, true, true], "DefaultOrder": 0, "PathName": "Aru", "DevName": "Aru", "Name": "Aru", "School": "Gehenna", "Club": "Kohshinjo68", "StarGrade": 3, "SquadType": "Main", "TacticRole": "DamageDealer", "Summons": [], "Position": "Back", "BulletType": "Explosion", "ArmorType": "LightArmor", "StreetBattleAdaptation": 4, "OutdoorBattleAdaptation": 2, "IndoorBattleAdaptation": 0, "WeaponType": "SR", "WeaponImg": "weapon_icon_10000", "Cover": true, "Equipment": ["Hat", "Hairpin", "Watch"], "CollectionBG": "BG_Gehenna_Collection", "FamilyName": "Rikuhachima", "PersonalName": "Aru", "SchoolYear": "2nd Year", "CharacterAge": "16 years old", "Birthday": "March 12th", "CharacterSSRNew": "Heh heh. Good choice.", "ProfileIntroduction": "The self-proclaimed boss of Problem Solver 68.\n\nAru expanded Gehenna Academy's Problem Solver 68 club without permission to conduct illegal business activities. She aspires to be seen as a dashing and debonair villain, but her frequent blunders always expose her as a poser.", "Hobby": "Studying management", "CharacterVoice": "Reina Kondō", "BirthDay": "3/12", "Illustrator": "DoReMi", "Designer": "DoReMi", "CharHeightMetric": "160cm", "CharHeightImperial": "5'2\"", "StabilityPoint": 1988, "AttackPower1": 369, "AttackPower100": 3690, "MaxHP1": 2236, "MaxHP100": 19390, "DefensePower1": 19, "DefensePower100": 119, "HealPower1": 1408, "HealPower100": 4225, "DodgePoint": 201, "AccuracyPoint": 905, "CriticalPoint": 201, "CriticalDamageRate": 20000, "AmmoCount": 5, "AmmoCost": 1, "Range": 750, "RegenCost": 700, "Skills": [{"SkillType": "autoattack", "Effects": [{"Type": "DMGSingle", "Hits": [10000], "Scale": [10000], "Frames": {"AttackEnterDuration": 45, "AttackStartDuration": 15, "AttackEndDuration": 16, "AttackBurstRoundOverDelay": 50, "AttackIngDuration": 42, "AttackReloadDuration": 70}, "CriticalCheck": "Check"}]}, {"SkillType": "ex", "Name": "Hard-Boiled Shot", "Desc": "Fires a shot that deals <?1> damage to one enemy and explodes for an additional <?2> damage to enemies in a circular area around the target.", "Parameters": [["274%", "315%", "397%", "438%", "521%"], ["292%", "335%", "423%", "467%", "554%"]], "Cost": [4, 4, 4, 4, 4], "Duration": 132, "Range": 950, "Radius": [{"Type": "Circle", "Radius": 200}], "Icon": "COMMON_SKILLICON_CIRCLE", "Effects": [{"Type": "DMGSingle", "CriticalCheck": "Check", "Scale": [27438, 31554, 39781, 43898, 52125]}, {"Type": "DMGSingle", "CriticalCheck": "Check", "Scale": [29207, 33584, 42344, 46727, 55487]}]}, {"SkillType": "normal", "Name": "Noir Attack", "Desc": "Every 25s, fires a shot that deals <?1> damage to one enemy with a 50% chance to explode for an additional <?2> damage to enemies in a circular area around the target.", "Parameters": [["152%", "160%", "168%", "198%", "206%", "213%", "244%", "251%", "259%", "290%"], ["251%", "263%", "276%", "326%", "338%", "351%", "401%", "414%", "426%", "476%"]], "Duration": 100, "Range": 950, "Radius": [{"Type": "Circle", "Radius": 100}], "Icon": "COMMON_SKILLICON_TARGET", "Effects": [{"Type": "DMGSingle", "CriticalCheck": "Check", "Scale": [15270, 16035, 16801, 19851, 20617, 21376, 24433, 25198, 25958, 29014]}, {"Type": "DMGSingle", "CriticalCheck": "Check", "Scale": [25102, 26355, 27608, 32631, 33884, 35137, 40155, 41414, 42667, 47685]}]}, {"SkillType": "gearnormal", "Name": "Noir Attack+", "Desc": "Every 25s, fires a shot that deals <?1> damage to one enemy with a 100% chance to explode for an additional <?2> damage to enemies in a circular area around the target.", "Parameters": [["156%", "163%", "171%", "203%", "210%", "218%", "249%", "257%", "265%", "296%"], ["256%", "269%", "282%", "333%", "346%", "359%", "410%", "423%", "436%", "487%"]], "Duration": 100, "Range": 950, "Radius": [{"Type": "Circle", "Radius": 100}], "Icon": "COMMON_SKILLICON_TARGET", "Effects": [{"Type": "DMGSingle", "CriticalCheck": "Check", "Scale": [15616, 16398, 17181, 20305, 21082, 21864, 24988, 25771, 26547, 29672]}, {"Type": "DMGSingle", "CriticalCheck": "Check", "Scale": [25669, 26956, 28237, 33368, 34656, 35937, 41068, 42355, 43637, 48768]}]}, {"SkillType": "passive", "Name": "The Gravity of a Boss", "Desc": "Increases <b:CriticalDamage> by <?1>.", "Parameters": [["14%", "14.7%", "15.4%", "18.2%", "18.9%", "19.6%", "22.4%", "23.1%", "23.8%", "26.6%"]], "Icon": "COMMON_SKILLICON_WEAPONBUFF", "Effects": [{"Type": "BuffSelf", "Stat": "CriticalDamageRate_Coefficient", "Scale": [1400, 1470, 1540, 1820, 1890, 1960, 2240, 2310, 2380, 2660]}]}, {"SkillType": "weaponpassive", "Name": "The Gravity of a Boss+", "Desc": "Increases <b:CriticalDamage> by <?1> plus a further <?2>.", "Parameters": [["2000", "2100", "2200", "2600", "2700", "2800", "3200", "3300", "3400", "3800"], ["14%", "14.7%", "15.4%", "18.2%", "18.9%", "19.6%", "22.4%", "23.1%", "23.8%", "26.6%"]], "Icon": "COMMON_SKILLICON_WEAPONBUFF", "Effects": [{"Type": "BuffSelf", "Stat": "CriticalDamageRate_Base", "Scale": [2000, 2100, 2200, 2600, 2700,Line truncated
|
||||||
+169
-6
@@ -1,19 +1,182 @@
|
|||||||
# will fix soon tm
|
"""FFmpeg media conversion cog.
|
||||||
"""FFmpeg media conversion cog — temporarily disabled.
|
|
||||||
|
|
||||||
Original commands: !togif, !caption, !reverse, !speed, !tomp4, !tomp3, !toopus.
|
Commands operate on a message attachment, or the attachment of a replied-to
|
||||||
Re-enabled once drawtext injection and resource-exhaustion concerns are addressed.
|
message: !togif, !caption <text>, !reverse, !speed <x>, !tomp4, !tomp3,
|
||||||
|
!toopus. All ffmpeg invocations use argument lists (no shell) and captions
|
||||||
|
are passed via a textfile to avoid drawtext injection.
|
||||||
"""
|
"""
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
import discord
|
||||||
from discord.ext import commands
|
from discord.ext import commands
|
||||||
|
|
||||||
|
MAX_UPLOAD = 50 * 1024 * 1024 # 50 MB input cap
|
||||||
|
MAX_DURATION = 600 # 10 min output cap
|
||||||
|
TIMEOUT = 120 # ffmpeg wall-clock cap (seconds)
|
||||||
|
|
||||||
|
FONT_CANDIDATES = [
|
||||||
|
"/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
|
||||||
|
"/usr/share/fonts/TTF/DejaVuSans-Bold.ttf",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
class FfmpegThings(commands.Cog):
|
class FfmpegThings(commands.Cog):
|
||||||
"""FFmpeg media utilities (disabled)."""
|
"""FFmpeg media utilities."""
|
||||||
|
|
||||||
def __init__(self, bot):
|
def __init__(self, bot: commands.Bot) -> None:
|
||||||
self.bot = bot
|
self.bot = bot
|
||||||
|
|
||||||
|
async def _get_attachment(self, message: discord.Message):
|
||||||
|
if message.attachments:
|
||||||
|
return message.attachments[0]
|
||||||
|
ref = message.reference
|
||||||
|
if ref is not None:
|
||||||
|
resolved = ref.resolved
|
||||||
|
if isinstance(resolved, discord.Message) and resolved.attachments:
|
||||||
|
return resolved.attachments[0]
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def _transcode(self, ctx: commands.Context, args: list[str], ext: str):
|
||||||
|
attachment = await self._get_attachment(ctx.message)
|
||||||
|
if attachment is None:
|
||||||
|
await ctx.reply("Attach a file, or reply to a message that has one.")
|
||||||
|
return
|
||||||
|
if attachment.size > MAX_UPLOAD:
|
||||||
|
await ctx.reply(f"File too big ({attachment.size // (1024*1024)} MB). Max {MAX_UPLOAD // (1024*1024)} MB.")
|
||||||
|
return
|
||||||
|
|
||||||
|
tmpdir = tempfile.mkdtemp(prefix="ffmpeg_")
|
||||||
|
inp = os.path.join(tmpdir, "in" + os.path.splitext(attachment.filename)[1])
|
||||||
|
out = os.path.join(tmpdir, "out" + ext)
|
||||||
|
try:
|
||||||
|
await attachment.save(inp)
|
||||||
|
await self._run_ffmpeg(["-i", inp, *args, out], tmpdir)
|
||||||
|
if not os.path.exists(out) or os.path.getsize(out) == 0:
|
||||||
|
raise RuntimeError("ffmpeg produced no output")
|
||||||
|
await ctx.reply(file=discord.File(out))
|
||||||
|
except Exception as exc:
|
||||||
|
await ctx.reply(f"ffmpeg failed: {exc}"[:2000])
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(tmpdir, ignore_errors=True)
|
||||||
|
|
||||||
|
async def _run_ffmpeg(self, args: list[str], tmpdir: str):
|
||||||
|
proc = await asyncio.create_subprocess_exec(
|
||||||
|
"ffmpeg", "-y", "-hide_banner", "-loglevel", "error",
|
||||||
|
"-t", str(MAX_DURATION), *args,
|
||||||
|
stdout=asyncio.subprocess.PIPE,
|
||||||
|
stderr=asyncio.subprocess.PIPE,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
_, stderr = await asyncio.wait_for(proc.communicate(), timeout=TIMEOUT)
|
||||||
|
except asyncio.TimeoutError:
|
||||||
|
proc.kill()
|
||||||
|
raise RuntimeError("ffmpeg timed out")
|
||||||
|
if proc.returncode != 0:
|
||||||
|
raise RuntimeError(stderr.decode(errors="replace")[:500])
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
async def togif(self, ctx: commands.Context):
|
||||||
|
"""Convert a video to a GIF."""
|
||||||
|
await self._transcode(
|
||||||
|
ctx,
|
||||||
|
["-vf", "fps=10,scale='min(480,iw)':-2:flags=lanczos"],
|
||||||
|
".gif",
|
||||||
|
)
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
async def tomp4(self, ctx: commands.Context):
|
||||||
|
"""Convert media to MP4 (h264/aac)."""
|
||||||
|
await self._transcode(
|
||||||
|
ctx,
|
||||||
|
["-c:v", "libx264", "-preset", "fast", "-crf", "23",
|
||||||
|
"-c:a", "aac", "-b:a", "128k", "-movflags", "+faststart"],
|
||||||
|
".mp4",
|
||||||
|
)
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
async def tomp3(self, ctx: commands.Context):
|
||||||
|
"""Extract audio to MP3."""
|
||||||
|
await self._transcode(
|
||||||
|
ctx,
|
||||||
|
["-vn", "-c:a", "libmp3lame", "-q:a", "2"],
|
||||||
|
".mp3",
|
||||||
|
)
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
async def toopus(self, ctx: commands.Context):
|
||||||
|
"""Extract audio to Opus."""
|
||||||
|
await self._transcode(
|
||||||
|
ctx,
|
||||||
|
["-vn", "-c:a", "libopus", "-b:a", "128k"],
|
||||||
|
".opus",
|
||||||
|
)
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
async def reverse(self, ctx: commands.Context):
|
||||||
|
"""Reverse a video/audio clip."""
|
||||||
|
await self._transcode(
|
||||||
|
ctx,
|
||||||
|
["-vf", "reverse", "-af", "areverse"],
|
||||||
|
".mp4",
|
||||||
|
)
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
async def speed(self, ctx: commands.Context, factor: float = 1.0):
|
||||||
|
"""Change playback speed (0.5 to 2.0)."""
|
||||||
|
if not 0.5 <= factor <= 2.0:
|
||||||
|
await ctx.reply("Speed must be between 0.5 and 2.0.")
|
||||||
|
return
|
||||||
|
await self._transcode(
|
||||||
|
ctx,
|
||||||
|
["-filter_complex",
|
||||||
|
f"[0:v]setpts=PTS/{factor}[v];[0:a]atempo={factor}[a]",
|
||||||
|
"-map", "[v]", "-map", "[a]"],
|
||||||
|
".mp4",
|
||||||
|
)
|
||||||
|
|
||||||
|
@commands.command()
|
||||||
|
async def caption(self, ctx: commands.Context, *, text: str):
|
||||||
|
"""Add a caption to the top of a video."""
|
||||||
|
text = text.strip()
|
||||||
|
if not text:
|
||||||
|
await ctx.reply("Provide a caption, e.g. `!caption hello world`.")
|
||||||
|
return
|
||||||
|
|
||||||
|
attachment = await self._get_attachment(ctx.message)
|
||||||
|
if attachment is None:
|
||||||
|
await ctx.reply("Attach a file, or reply to a message that has one.")
|
||||||
|
return
|
||||||
|
if attachment.size > MAX_UPLOAD:
|
||||||
|
await ctx.reply(f"File too big ({attachment.size // (1024*1024)} MB).")
|
||||||
|
return
|
||||||
|
|
||||||
|
font = next((f for f in FONT_CANDIDATES if os.path.exists(f)), None)
|
||||||
|
tmpdir = tempfile.mkdtemp(prefix="ffmpeg_cap_")
|
||||||
|
inp = os.path.join(tmpdir, "in" + os.path.splitext(attachment.filename)[1])
|
||||||
|
caption_file = os.path.join(tmpdir, "caption.txt")
|
||||||
|
out = os.path.join(tmpdir, "out.mp4")
|
||||||
|
try:
|
||||||
|
await attachment.save(inp)
|
||||||
|
with open(caption_file, "w", encoding="utf-8") as f:
|
||||||
|
f.write(text)
|
||||||
|
font_arg = f":fontfile={font}" if font else ""
|
||||||
|
vf = (
|
||||||
|
f"drawtext=textfile={caption_file}:fontsize=40:fontcolor=white"
|
||||||
|
f":box=1:[email protected]:boxborderw=10:x=(w-text_w)/2:y=20{font_arg}"
|
||||||
|
)
|
||||||
|
await self._run_ffmpeg(
|
||||||
|
["-i", inp, "-vf", vf, "-c:a", "copy", out],
|
||||||
|
tmpdir,
|
||||||
|
)
|
||||||
|
await ctx.reply(file=discord.File(out))
|
||||||
|
except Exception as exc:
|
||||||
|
await ctx.reply(f"ffmpeg failed: {exc}"[:2000])
|
||||||
|
finally:
|
||||||
|
shutil.rmtree(tmpdir, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
async def setup(bot: commands.Bot) -> None:
|
async def setup(bot: commands.Bot) -> None:
|
||||||
await bot.add_cog(FfmpegThings(bot))
|
await bot.add_cog(FfmpegThings(bot))
|
||||||
@@ -48,7 +48,7 @@ async def main():
|
|||||||
await bot.load_extension('cogs.sysinfo')
|
await bot.load_extension('cogs.sysinfo')
|
||||||
await bot.load_extension('music.music_wrapper')
|
await bot.load_extension('music.music_wrapper')
|
||||||
await bot.load_extension('cogs.statchannel')
|
await bot.load_extension('cogs.statchannel')
|
||||||
# await bot.load_extension('cogs.ffmpeg_things') # will fix soon tm
|
await bot.load_extension('cogs.ffmpeg_things')
|
||||||
await bot.load_extension('cogs.roles')
|
await bot.load_extension('cogs.roles')
|
||||||
await bot.load_extension('cogs.gitpull')
|
await bot.load_extension('cogs.gitpull')
|
||||||
await bot.load_extension('cogs.currency_converter')
|
await bot.load_extension('cogs.currency_converter')
|
||||||
|
|||||||
Reference in new issue
Block a user