mirror of
https://github.com/senju1337/senju.git
synced 2025-12-24 07:39:29 +00:00
Merge branch 'devel' into feat/OPS-56
This commit is contained in:
commit
4e19b01257
16 changed files with 547 additions and 94 deletions
|
|
@ -1,2 +1,2 @@
|
|||
[run]
|
||||
omit = tests/*
|
||||
omit = tests/*
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -180,4 +180,3 @@ ollama
|
|||
*.kate-swp
|
||||
# sphinx rst files
|
||||
docs/source/_modules
|
||||
|
||||
|
|
|
|||
0
docs/auto_docu.sh
Normal file → Executable file
0
docs/auto_docu.sh
Normal file → Executable file
|
|
@ -3,10 +3,13 @@
|
|||
# For the full list of built-in configuration values, see the documentation:
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# -- Project information -----------------------------------------------------
|
||||
# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.abspath("../../senju"))
|
||||
|
||||
project = 'senju'
|
||||
|
|
|
|||
|
|
@ -16,4 +16,4 @@ documentation for details.
|
|||
:caption: Contents:
|
||||
|
||||
usage
|
||||
_modules/modules
|
||||
_modules/modules
|
||||
|
|
|
|||
25
poetry.lock
generated
25
poetry.lock
generated
|
|
@ -1,4 +1,4 @@
|
|||
# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "alabaster"
|
||||
|
|
@ -25,7 +25,7 @@ files = [
|
|||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""]
|
||||
dev = ["backports.zoneinfo", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata"]
|
||||
|
||||
[[package]]
|
||||
name = "blinker"
|
||||
|
|
@ -255,7 +255,7 @@ files = [
|
|||
]
|
||||
|
||||
[package.extras]
|
||||
toml = ["tomli ; python_full_version <= \"3.11.0a6\""]
|
||||
toml = ["tomli"]
|
||||
|
||||
[[package]]
|
||||
name = "docutils"
|
||||
|
|
@ -514,6 +514,21 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""}
|
|||
[package.extras]
|
||||
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-httpserver"
|
||||
version = "1.1.2"
|
||||
description = "pytest-httpserver is a httpserver for pytest"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "pytest_httpserver-1.1.2-py3-none-any.whl", hash = "sha256:93009d79574fc982301e8494fdea0884f21bb0caf3bcc719151dfbd1e3a943ea"},
|
||||
{file = "pytest_httpserver-1.1.2.tar.gz", hash = "sha256:38d0b726580d05c47cbd0ced1ecb36a51668ef1596cdc6d70a9cfa2b3cc00ebd"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
Werkzeug = ">=2.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.32.3"
|
||||
|
|
@ -752,7 +767,7 @@ files = [
|
|||
]
|
||||
|
||||
[package.extras]
|
||||
brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""]
|
||||
brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
|
||||
h2 = ["h2 (>=4,<5)"]
|
||||
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||
zstd = ["zstandard (>=0.18.0)"]
|
||||
|
|
@ -778,4 +793,4 @@ watchdog = ["watchdog (>=2.3)"]
|
|||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.10"
|
||||
content-hash = "ce9cac7092447dc5d6b3920853bb10fcc166018bc4f48c50925ef09db74e7891"
|
||||
content-hash = "76f74a5c2443d5c8e9db9433f0bdcc94a1d12c469f8b49dd325f3cc5a1b06f4e"
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ dependencies = [
|
|||
"tinydb (>=3.1.0,<4.0.0)",
|
||||
"requests (>=2.32.3,<3.0.0)",
|
||||
"coverage (>=7.6.12,<8.0.0)",
|
||||
"pytest-httpserver (>=1.1.2,<2.0.0)",
|
||||
]
|
||||
|
||||
|
||||
|
|
|
|||
127
senju/haiku.py
127
senju/haiku.py
|
|
@ -1,3 +1,66 @@
|
|||
"""
|
||||
Haiku Generation Module
|
||||
=======================
|
||||
|
||||
A client interface for AI-powered haiku poem generation.
|
||||
|
||||
This module provides the core functionality for communicating
|
||||
with an Ollama-based
|
||||
AI service to generate three-line haiku poems. It handles the
|
||||
entire generation
|
||||
process, from sending properly formatted requests to processing
|
||||
and validating
|
||||
the returned poems.
|
||||
|
||||
Classes
|
||||
-------
|
||||
Haiku
|
||||
A dataclass representation of a haiku poem, providing structure
|
||||
for storage,
|
||||
manipulation and serialization of poem data.
|
||||
|
||||
**Methods**:
|
||||
|
||||
* ``to_json()``: Converts a haiku instance to JSON format for API
|
||||
responses
|
||||
* ``generate_haiku(seed_text)``: Creates a new haiku using
|
||||
the AI service
|
||||
|
||||
Constants
|
||||
---------
|
||||
AI_SERVICE_URL
|
||||
The endpoint URL for the Ollama API service.
|
||||
|
||||
AI_MODEL_NAME
|
||||
The specific AI model used for haiku generation.
|
||||
|
||||
REQUEST_TIMEOUT
|
||||
The maximum time (in seconds) to wait for AI service responses.
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
* requests: HTTP client library for API communication
|
||||
* dataclasses: Support for the Haiku data structure
|
||||
* logging: Error and diagnostic information capture
|
||||
* json: Processing of API responses
|
||||
|
||||
Implementation Details
|
||||
----------------------
|
||||
The module implements a robust communication pattern with the
|
||||
AI service, including:
|
||||
|
||||
1. Proper request formatting with seed text integration
|
||||
2. Multiple retry attempts for handling temporary service issues
|
||||
3. Response validation to ensure the returned text follows haiku structure
|
||||
4. Fallback mechanisms when the AI service returns unsuitable content
|
||||
5. JSON serialization for consistent data exchange
|
||||
|
||||
When communicating with the AI service, the module maintains appropriate
|
||||
error handling and logging to help diagnose any generation issues. It aims
|
||||
to provide a reliable haiku generation experience even when dealing with the
|
||||
inherent unpredictability of AI-generated content.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
|
|
@ -12,16 +75,43 @@ AI_GEN_ENDPOINT: str = "/generate"
|
|||
|
||||
@dataclass
|
||||
class Haiku:
|
||||
"""
|
||||
A class representing a haiku poem with three lines.
|
||||
|
||||
:ivar lines: A list containing the three lines of the haiku.
|
||||
:type lines: list[str]
|
||||
"""
|
||||
lines: list[str]
|
||||
|
||||
def get_json(self):
|
||||
"""
|
||||
Converts the haiku lines to a JSON string.
|
||||
|
||||
:return: A JSON string representation of the haiku lines.
|
||||
:rtype: str
|
||||
"""
|
||||
return json.dumps(self.lines)
|
||||
|
||||
@staticmethod
|
||||
def request_haiku(seed: str) -> Haiku:
|
||||
"""This function prompts the ai to generate
|
||||
the hauku based on the user input"""
|
||||
def request_haiku(seed: str, url=AI_BASE_URL + AI_GEN_ENDPOINT) -> Haiku:
|
||||
"""
|
||||
Generates a haiku using an AI model based on the
|
||||
provided seed text.
|
||||
|
||||
This function prompts the AI to generate a haiku based on the
|
||||
user input.
|
||||
It validates that the response contains exactly 3 lines.
|
||||
The function will retry until a valid haiku is generated.
|
||||
|
||||
:param seed: The input text used to inspire the haiku generation.
|
||||
:param url: The URL to the AI endpoint
|
||||
:type seed: str
|
||||
:return: A new Haiku object containing the generated three lines.
|
||||
:rtype: Haiku
|
||||
|
||||
:raises: Possible JSONDecodeError which is caught and handled
|
||||
with retries.
|
||||
"""
|
||||
ai_gen_request = {
|
||||
"model": "haiku",
|
||||
"prompt": f"{seed}",
|
||||
|
|
@ -29,23 +119,33 @@ class Haiku:
|
|||
"eval_count": 20
|
||||
}
|
||||
|
||||
tries = 0
|
||||
|
||||
while True:
|
||||
tries += 1
|
||||
try:
|
||||
r = requests.post(url=AI_BASE_URL + AI_GEN_ENDPOINT,
|
||||
r = requests.post(url=url,
|
||||
json=ai_gen_request)
|
||||
ai_response = str(r.json()["response"])
|
||||
|
||||
logging.warning(ai_response)
|
||||
logging.debug(f"ai response: {ai_response}")
|
||||
|
||||
lines = ai_response.split("\n")
|
||||
|
||||
while len(lines) != 3:
|
||||
lines.pop()
|
||||
|
||||
logging.warning(lines)
|
||||
logging.info(f"lines for haiku: {lines}")
|
||||
|
||||
if len(lines) != 3:
|
||||
continue
|
||||
if len(lines) < 3:
|
||||
if tries < 20:
|
||||
logging.warning("too few lines, trying again")
|
||||
logging.debug(lines)
|
||||
continue
|
||||
else:
|
||||
logging.warning("too many tries, aborting")
|
||||
raise Exception(
|
||||
"Generating the haiku took too many tries")
|
||||
|
||||
haiku = Haiku(
|
||||
[
|
||||
|
|
@ -53,9 +153,14 @@ class Haiku:
|
|||
lines[1],
|
||||
lines[2]
|
||||
])
|
||||
|
||||
break
|
||||
except json.JSONDecodeError:
|
||||
continue
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logging.error(f"error while reading json from LLM: {e}")
|
||||
raise e
|
||||
|
||||
return haiku
|
||||
|
||||
|
||||
DEFAULT_HAIKU: Haiku = Haiku(["Purple petals rise", "Defying fragile beauty",
|
||||
"Fiercely breathing life"])
|
||||
|
|
|
|||
117
senju/main.py
117
senju/main.py
|
|
@ -1,15 +1,57 @@
|
|||
"""
|
||||
Senju Haiku Web Application
|
||||
===========================
|
||||
|
||||
A Flask-based web interface for generating, viewing, and managing haiku poetry.
|
||||
|
||||
This application provides a comprehensive interface between users
|
||||
and an AI-powered
|
||||
haiku generation service, with persistent storage capabilities.
|
||||
Users can interact
|
||||
with the system through both a web interface and a RESTful API.
|
||||
|
||||
Features
|
||||
--------
|
||||
* **Landing page**: Welcome interface introducing users to the Senju service
|
||||
* **Browsing interface**: Gallery-style viewing of previously generated haikus
|
||||
* **Prompt interface**: Text input system for generating haikus from seed text
|
||||
* **Image scanning**: Experimental interface for creating haikus
|
||||
from visual inputs
|
||||
* **RESTful API**: Programmatic access for integration with other services
|
||||
|
||||
Architecture
|
||||
------------
|
||||
The application implements a RESTful architecture using Flask's routing system
|
||||
and template rendering. All user interactions are handled through
|
||||
clearly defined
|
||||
routes, with appropriate error handling for exceptional cases.
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
* future.annotations: Enhanced type hint support
|
||||
* os, Path: Filesystem operations for storage management
|
||||
* Flask: Core web application framework
|
||||
* Haiku: Custom class for poem representation and generation
|
||||
* StoreManager: Database abstraction for persistence operations
|
||||
|
||||
Implementation
|
||||
--------------
|
||||
The module initializes both a Flask application instance and a StoreManager
|
||||
with a configured storage location. All routes and view functions required
|
||||
for the complete web interface are defined within this module.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from flask import (Flask, redirect, render_template, request, url_for,
|
||||
send_from_directory)
|
||||
from flask import (Flask, redirect, render_template, request,
|
||||
send_from_directory, url_for)
|
||||
|
||||
from senju.haiku import Haiku
|
||||
from senju.store_manager import StoreManager
|
||||
|
||||
import os
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
store = StoreManager(Path("/tmp/store.db"))
|
||||
|
|
@ -17,29 +59,55 @@ store = StoreManager(Path("/tmp/store.db"))
|
|||
|
||||
@app.route("/")
|
||||
def index_view():
|
||||
"""
|
||||
Render the main index page of the application.
|
||||
|
||||
:return: The index.html template with title "Senju".
|
||||
:rtype: flask.Response
|
||||
"""
|
||||
return render_template("index.html", title="Senju")
|
||||
|
||||
|
||||
@app.route("/haiku/")
|
||||
def haiku_index_view():
|
||||
"""
|
||||
Redirect to the most recently created haiku.
|
||||
|
||||
:return: Redirects to the haiku_view route with the latest haiku ID.
|
||||
:rtype: flask.Response
|
||||
:raises KeyError: If no haikus exist in the store yet.
|
||||
"""
|
||||
haiku_id: int | None = store.get_id_of_latest_haiku()
|
||||
if haiku_id is None:
|
||||
# TODO: add "empty haiku list" error page
|
||||
raise KeyError("no haiku exist yet")
|
||||
return redirect(url_for("haiku_view", haiku_id=haiku_id))
|
||||
haiku_id = 0
|
||||
return redirect(url_for("haiku_view", haiku_id=haiku_id, is_default=1))
|
||||
|
||||
|
||||
@app.route("/haiku/<int:haiku_id>")
|
||||
def haiku_view(haiku_id):
|
||||
"""test"""
|
||||
"""
|
||||
Display a specific haiku by its ID.
|
||||
|
||||
Loads the haiku with the given ID from the store and renders it using
|
||||
the haiku.html template. If no haiku is found with the provided ID,
|
||||
raises a KeyError.
|
||||
|
||||
:param haiku_id: The ID of the haiku to display.
|
||||
:type haiku_id: int
|
||||
:return: The haiku.html template with the haiku data in context.
|
||||
:rtype: flask.Response
|
||||
:raises KeyError: If no haiku exists with the given ID.
|
||||
"""
|
||||
haiku: Haiku | None = store.load_haiku(haiku_id)
|
||||
if haiku is None:
|
||||
# TODO: add "haiku not found" page
|
||||
raise KeyError("haiku not found")
|
||||
is_default: bool = request.args.get("is_default") == "1"
|
||||
haiku: Haiku = store.load_haiku(haiku_id)
|
||||
context: dict = {
|
||||
"haiku": haiku
|
||||
"haiku": haiku,
|
||||
"is_default": is_default
|
||||
}
|
||||
|
||||
return render_template(
|
||||
"haiku.html",
|
||||
context=context,
|
||||
|
|
@ -48,6 +116,12 @@ def haiku_view(haiku_id):
|
|||
|
||||
@app.route("/prompt")
|
||||
def prompt_view():
|
||||
"""
|
||||
Render the haiku generation prompt page.
|
||||
|
||||
:return: The prompt.html template with title "Haiku generation".
|
||||
:rtype: flask.Response
|
||||
"""
|
||||
return render_template(
|
||||
"prompt.html",
|
||||
title="Haiku generation"
|
||||
|
|
@ -56,6 +130,12 @@ def prompt_view():
|
|||
|
||||
@app.route("/scan")
|
||||
def scan_view():
|
||||
"""
|
||||
Render the image scanning page.
|
||||
|
||||
:return: The scan.html template with title "Image scanning".
|
||||
:rtype: flask.Response
|
||||
"""
|
||||
return render_template(
|
||||
"scan.html",
|
||||
title="Image scanning"
|
||||
|
|
@ -64,6 +144,17 @@ def scan_view():
|
|||
|
||||
@app.route("/api/v1/haiku", methods=['POST'])
|
||||
def generate_haiku():
|
||||
"""
|
||||
API endpoint to generate a new haiku based on the provided prompt.
|
||||
|
||||
Accepts POST requests with JSON data containing a 'prompt' field.
|
||||
Generates a haiku using the prompt, saves it to the store,
|
||||
and returns the ID.
|
||||
|
||||
:return: The ID of the newly created haiku if method is POST.
|
||||
Error message and status code 405 if method is not POST.
|
||||
:rtype: Union[str, Tuple[str, int]]
|
||||
"""
|
||||
if request.method == 'POST':
|
||||
json_data = request.get_json()
|
||||
prompt = json_data["prompt"]
|
||||
|
|
@ -78,6 +169,12 @@ def generate_haiku():
|
|||
|
||||
@app.route('/favicon.ico')
|
||||
def favicon():
|
||||
"""
|
||||
Serve the favicon.ico file from the static directory.
|
||||
|
||||
:return: The favicon.ico file with the appropriate MIME type.
|
||||
:rtype: flask.Response
|
||||
"""
|
||||
return send_from_directory(os.path.join(app.root_path, 'static/img'),
|
||||
'favicon.ico',
|
||||
mimetype='image/vnd.microsoft.icon')
|
||||
|
|
|
|||
|
|
@ -1,3 +1,53 @@
|
|||
"""
|
||||
Senju Database Management Module
|
||||
================================
|
||||
|
||||
A database interaction layer for the Senju haiku management system.
|
||||
|
||||
This module implements a lightweight document database
|
||||
abstraction using TinyDB
|
||||
for persistent storage of haiku poems. It provides a
|
||||
clean interface for storing,
|
||||
retrieving, updating, and managing haiku entries in the system.
|
||||
|
||||
Classes
|
||||
-------
|
||||
StoreManager
|
||||
The primary class responsible for all database operations.
|
||||
Handles connection
|
||||
management, CRUD operations, and query capabilities for haiku data.
|
||||
|
||||
Functions
|
||||
---------
|
||||
utility_function
|
||||
Provides simple arithmetic operations to support
|
||||
database functionalities.
|
||||
|
||||
Constants
|
||||
---------
|
||||
DEFAULT_DB_PATH
|
||||
The default filesystem location for the TinyDB database file
|
||||
(/var/lib/senju.json).
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
* future.annotations: Enhanced type hint support
|
||||
* logging.Logger: Diagnostic and error logging capabilities
|
||||
* pathlib.Path: Cross-platform filesystem path handling
|
||||
* typing.Optional: Type annotations for nullable values
|
||||
* tinydb.TinyDB: Lightweight document database implementation
|
||||
* tinydb.QueryImpl: Query builder for database searches
|
||||
* senju.haiku.Haiku: Data model for haiku representation
|
||||
|
||||
Implementation Details
|
||||
----------------------
|
||||
The module uses TinyDB as its storage engine, providing a JSON-based document
|
||||
storage solution that balances simplicity with functionality. The StoreManager
|
||||
abstracts all database operations behind a clean API,
|
||||
handling connection lifecycle
|
||||
and providing methods for common operations on haiku data.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from logging import Logger
|
||||
|
|
@ -7,24 +57,79 @@ from typing import Optional
|
|||
from tinydb import TinyDB
|
||||
from tinydb.queries import QueryImpl
|
||||
|
||||
from senju.haiku import Haiku
|
||||
from senju.haiku import DEFAULT_HAIKU, Haiku
|
||||
|
||||
DEFAULT_DB_PATH: Path = Path("/var/lib/senju.json")
|
||||
|
||||
|
||||
class BadStoreManagerFileError(Exception):
|
||||
def __init__(self, msg: str, * args: object) -> None:
|
||||
self.msg = msg
|
||||
super().__init__(*args)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"Store file is corrupted: {self.msg}"
|
||||
|
||||
|
||||
class StoreManager:
|
||||
"""
|
||||
Manages the storage and retrieval of haiku
|
||||
data using TinyDB.
|
||||
|
||||
This class provides an interface for saving and
|
||||
loading haikus from
|
||||
a TinyDB database file.
|
||||
|
||||
:ivar _db: Database instance for storing haiku data.
|
||||
:type _db: TinyDB
|
||||
:ivar logger: Logger for tracking operations and errors.
|
||||
:type logger: Logger
|
||||
"""
|
||||
__slots__ = "_db", "logger"
|
||||
_db: TinyDB
|
||||
logger: Logger
|
||||
|
||||
def __init__(self, path_to_db: Path = DEFAULT_DB_PATH) -> None:
|
||||
"""
|
||||
Initialize the StoreManager with a database path.
|
||||
|
||||
:param path_to_db: Path to the TinyDB database file.
|
||||
Defaults to DEFAULT_DB_PATH.
|
||||
:type path_to_db: Path, optional
|
||||
:return: None
|
||||
"""
|
||||
self._db = TinyDB(path_to_db)
|
||||
|
||||
try:
|
||||
self._db = TinyDB(path_to_db)
|
||||
except Exception as e:
|
||||
raise BadStoreManagerFileError(f"{e}")
|
||||
self.logger = Logger(__name__)
|
||||
|
||||
def _query(self, query: QueryImpl) -> list[dict]:
|
||||
"""
|
||||
Execute a query against the database.
|
||||
|
||||
:param query: TinyDB query to execute.
|
||||
:type query: QueryImpl
|
||||
:return: List of documents matching the query.
|
||||
:rtype: list[dict]
|
||||
"""
|
||||
return self._db.search(query)
|
||||
|
||||
def _load(self, id: int) -> Optional[dict]:
|
||||
"""
|
||||
Load a document by its ID.
|
||||
|
||||
:param id: Document ID to load.
|
||||
:type id: int
|
||||
:return: The document if found, None otherwise.
|
||||
:rtype: Optional[dict]
|
||||
|
||||
.. note::
|
||||
Logs a warning if document with specified
|
||||
ID is not found.
|
||||
"""
|
||||
try:
|
||||
return self._db.get(doc_id=id)
|
||||
except IndexError as e:
|
||||
|
|
@ -32,19 +137,55 @@ class StoreManager:
|
|||
return None
|
||||
|
||||
def _save(self, data: dict) -> int:
|
||||
"""
|
||||
Save a document to the database.
|
||||
|
||||
:param data: Document data to save.
|
||||
:type data: dict
|
||||
:return: The document ID of the saved document.
|
||||
:rtype: int
|
||||
"""
|
||||
return self._db.insert(data)
|
||||
|
||||
def load_haiku(self, key: int) -> Optional[Haiku]:
|
||||
def load_haiku(self, key: Optional[int]) -> Haiku:
|
||||
"""
|
||||
Load a haiku by its ID.
|
||||
|
||||
:param key: The ID of the haiku to load.
|
||||
:type key: int
|
||||
:return: A Haiku object if found, None otherwise.
|
||||
:rtype: Optional[Haiku]
|
||||
"""
|
||||
if key is None:
|
||||
return DEFAULT_HAIKU
|
||||
raw_haiku: dict | None = self._load(key)
|
||||
if raw_haiku is None:
|
||||
return None
|
||||
return DEFAULT_HAIKU
|
||||
h = Haiku(**raw_haiku)
|
||||
return h
|
||||
|
||||
def save_haiku(self, data: Haiku) -> int:
|
||||
"""
|
||||
Save a haiku to the database.
|
||||
|
||||
:param data: The Haiku object to save.
|
||||
:type data: Haiku
|
||||
:return: The document ID of the saved haiku.
|
||||
:rtype: int
|
||||
"""
|
||||
return self._save(data.__dict__)
|
||||
|
||||
def get_id_of_latest_haiku(self) -> Optional[int]:
|
||||
"""
|
||||
Get the ID of the most recently added haiku.
|
||||
|
||||
:return: The ID of the latest haiku if any exists,
|
||||
None otherwise.
|
||||
:rtype: Optional[int]
|
||||
|
||||
.. note::
|
||||
Logs an error if the database is empty.
|
||||
"""
|
||||
try:
|
||||
id = self._db.all()[-1].doc_id
|
||||
return id
|
||||
|
|
|
|||
|
|
@ -21,6 +21,11 @@
|
|||
</select>
|
||||
</div>
|
||||
</div>
|
||||
{% if context.is_default %}
|
||||
<div class="mb-5">
|
||||
<b>Note:</b> No haikus have been found in the haiku store.
|
||||
</div>
|
||||
{% endif %}
|
||||
<a href="{{ url_for('index_view') }}"
|
||||
class="inline-block bg-violet-600 hover:bg-violet-700 text-white font-bold py-2 px-4 rounded-lg mt-6">
|
||||
Back to Home
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
<input
|
||||
type="text"
|
||||
id="user-input"
|
||||
minlength="0"
|
||||
minlength="0"
|
||||
maxlength="100"
|
||||
placeholder="Type your prompt here..."
|
||||
class="w-full px-4 py-3 text-lg border-2 border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-violet-600"
|
||||
|
|
@ -39,9 +39,6 @@ document.getElementById("submit-btn").addEventListener("click", function() {
|
|||
|
||||
if (userInput.trim() === "") {
|
||||
responseText.textContent = "Please enter a prompt!";
|
||||
}
|
||||
else if (userInput.length > 100) {
|
||||
responseText.textContent = "Input must under 100 characters long!";
|
||||
}
|
||||
else if (userInput.trim() === "amogus") {
|
||||
responseText.textContent = "🤖 AI is thinking...";
|
||||
|
|
@ -51,7 +48,7 @@ document.getElementById("submit-btn").addEventListener("click", function() {
|
|||
setTimeout(() => {
|
||||
responseText.textContent = "Sus imposter ඞ";
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
else {
|
||||
responseText.textContent = "🤖 AI is thinking...";
|
||||
responseBox.classList.remove("opacity-0");
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
<body>
|
||||
<div class="flex flex-col items-center justify-center min-h-screen bg-violet-900 text-white p-6">
|
||||
<div class="bg-white text-gray-900 p-8 rounded-xl shadow-lg max-w-lg w-full text-center transform transition duration-300 hover:scale-105 mb-8">
|
||||
|
||||
|
||||
<h1 class="text-3xl font-bold text-violet-700 mb-4">Upload your image</h1>
|
||||
<!-- File Upload container-->
|
||||
<div id="upload-area" class="flex items-center justify-center w-full">
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
<input id="dropzone-file" type="file" accept="image/*" class="hidden" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Image Preview container-->
|
||||
<div id="image-preview" class="w-full hidden">
|
||||
<div class="relative">
|
||||
|
|
@ -39,17 +39,17 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Error message -->
|
||||
<div id="error-message" class="mt-4 text-red-500 hidden">
|
||||
Please upload an image first.
|
||||
</div>
|
||||
|
||||
|
||||
<button id="submit-button" type="submit" class="mt-6 bg-violet-600 hover:bg-violet-700 text-white font-bold py-2 px-4 rounded transition duration-300">
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div id="response-box" class="mt-8 bg-white text-gray-900 p-6 rounded-lg shadow-lg max-w-lg w-full text-center opacity-0 transition-opacity duration-500 ease-in-out">
|
||||
<h2 class="text-2xl font-semibold text-violet-700">AI recognized the following:</h2>
|
||||
<p id="ai-response" class="text-lg text-gray-700 mt-2 italic">Waiting for input...</p>
|
||||
|
|
@ -62,7 +62,7 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- New generating haiku div that appears after "Yes" is clicked -->
|
||||
<div id="generating-haiku-box" class="mt-8 bg-white text-gray-900 p-6 rounded-lg shadow-lg max-w-lg w-full text-center hidden transition-opacity duration-500 ease-in-out">
|
||||
<h2 class="text-2xl font-semibold text-violet-700">Generating Haiku</h2>
|
||||
|
|
@ -103,24 +103,24 @@ Vote him, task complete.</h2>
|
|||
const yesButton = document.getElementById('yes-button');
|
||||
const noButton = document.getElementById('no-button');
|
||||
const generatingHaikuBox = document.getElementById('generating-haiku-box');
|
||||
const generatedHaikuBox = document.getElementById('generated-haiku-box');
|
||||
const generatedHaikuBox = document.getElementById('generated-haiku-box');
|
||||
let imageUploaded = false;
|
||||
|
||||
function handleFileSelect(event) {
|
||||
const file = event.target.files[0];
|
||||
|
||||
|
||||
if (file && file.type.startsWith('image/')) {
|
||||
// Create a URL for the selected image
|
||||
const imageUrl = URL.createObjectURL(file);
|
||||
|
||||
|
||||
// Set the image source
|
||||
previewImg.src = imageUrl;
|
||||
|
||||
|
||||
// Hide upload area and show image preview
|
||||
uploadArea.classList.add('hidden');
|
||||
imagePreview.classList.remove('hidden');
|
||||
errorMessage.classList.add('hidden');
|
||||
|
||||
|
||||
// Set flag that image is uploaded
|
||||
imageUploaded = true;
|
||||
}
|
||||
|
|
@ -128,14 +128,14 @@ Vote him, task complete.</h2>
|
|||
|
||||
function removeImage() {
|
||||
dropzoneFile.value = '';
|
||||
|
||||
// Hide image
|
||||
|
||||
// Hide image
|
||||
imagePreview.classList.add('hidden');
|
||||
uploadArea.classList.remove('hidden');
|
||||
|
||||
|
||||
URL.revokeObjectURL(previewImg.src);
|
||||
previewImg.src = '';
|
||||
|
||||
|
||||
imageUploaded = false;
|
||||
responseBox.classList.add('opacity-0');
|
||||
generatingHaikuBox.classList.add('hidden');
|
||||
|
|
@ -148,32 +148,33 @@ Vote him, task complete.</h2>
|
|||
if (imageUploaded) {
|
||||
// Hide error
|
||||
errorMessage.classList.add('hidden');
|
||||
|
||||
// Show response box
|
||||
|
||||
// Show response box
|
||||
responseBox.classList.remove('opacity-0');
|
||||
|
||||
|
||||
// Example response
|
||||
document.getElementById('ai-response').textContent = 'Example text';
|
||||
|
||||
} else {
|
||||
|
||||
|
||||
errorMessage.classList.remove('hidden');
|
||||
|
||||
|
||||
uploadArea.classList.add('shake');
|
||||
setTimeout(() => {
|
||||
uploadArea.classList.remove('shake');
|
||||
}, 600);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function handleYesClick() {
|
||||
// Hide response box
|
||||
responseBox.classList.add('opacity-0');
|
||||
|
||||
|
||||
// Show generating haiku box first
|
||||
setTimeout(() => {
|
||||
responseBox.classList.add('hidden');
|
||||
generatingHaikuBox.classList.remove('hidden');
|
||||
|
||||
|
||||
// After a delay, hide generating box and show result
|
||||
setTimeout(() => {
|
||||
generatingHaikuBox.classList.add('hidden');
|
||||
|
|
@ -184,13 +185,13 @@ Vote him, task complete.</h2>
|
|||
// Reset everything
|
||||
removeImage();
|
||||
}
|
||||
|
||||
|
||||
dropzoneFile.addEventListener('change', handleFileSelect);
|
||||
removeImageBtn.addEventListener('click', removeImage);
|
||||
submitButton.addEventListener('click', handleSubmit);
|
||||
yesButton.addEventListener('click', handleYesClick);
|
||||
noButton.addEventListener('click', handleNoClick);
|
||||
|
||||
|
||||
// Add some CSS animation
|
||||
document.head.insertAdjacentHTML('beforeend', `
|
||||
<style>
|
||||
|
|
|
|||
68
tests/test_haiku.py
Normal file
68
tests/test_haiku.py
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# I do not trust python and it's tests, so I'm testing them. May not be worth
|
||||
# much, but at least it shows me a few things.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pytest_httpserver import HTTPServer
|
||||
import requests
|
||||
|
||||
# do not remove this import. This is needed for
|
||||
# pytest fixtures to work
|
||||
import pytest # noqa: F401
|
||||
|
||||
from senju.haiku import Haiku # noqa: F401
|
||||
|
||||
# Note: these weird arguments are an indicator of what should be dome
|
||||
# before. For example, `temp_data_dir` is a function in `conftest.py`. If we
|
||||
# put it in the arguments, it seems to run before our test, and the return
|
||||
# value becomes a local. This is all very confusing for someone used to
|
||||
# Rust's libtest
|
||||
|
||||
|
||||
def test_create_haiku():
|
||||
haiku = Haiku(["line number 1", "line number 2", "line number 3"])
|
||||
assert haiku.lines[0] == "line number 1"
|
||||
assert haiku.lines[1] == "line number 2"
|
||||
assert haiku.lines[2] == "line number 3"
|
||||
assert len(haiku.lines) == 3
|
||||
|
||||
|
||||
def test_get_haiku_json():
|
||||
haiku = Haiku(["line number 1", "line number 2", "line number 3"])
|
||||
data_raw: str = haiku.get_json()
|
||||
assert data_raw == '["line number 1", "line number 2", "line number 3"]'
|
||||
data = json.loads(data_raw)
|
||||
assert haiku.lines[0] == "line number 1"
|
||||
assert haiku.lines[1] == "line number 2"
|
||||
assert haiku.lines[2] == "line number 3"
|
||||
assert len(haiku.lines) == 3
|
||||
assert data == ['line number 1', 'line number 2', 'line number 3']
|
||||
|
||||
|
||||
def test_request_haiku(httpserver: HTTPServer):
|
||||
|
||||
httpserver.expect_request(
|
||||
"/testhaiku").respond_with_json({"response":
|
||||
"The apparition of these\n"
|
||||
"faces in a crowd; Petal\n"
|
||||
"on a wet, black bough."
|
||||
})
|
||||
|
||||
haiku = Haiku.request_haiku(
|
||||
"apple banana papaya", url=httpserver.url_for("/testhaiku"))
|
||||
assert haiku.lines[0] == "The apparition of these"
|
||||
assert haiku.lines[1] == "faces in a crowd; Petal"
|
||||
assert haiku.lines[2] == "on a wet, black bough."
|
||||
assert len(haiku.lines) == 3
|
||||
|
||||
|
||||
def test_request_haiku_respondse_bad(httpserver: HTTPServer):
|
||||
with pytest.raises(requests.exceptions.JSONDecodeError):
|
||||
|
||||
httpserver.expect_request(
|
||||
"/testhaiku").respond_with_data(
|
||||
"this is completely wrong" + ("A" * 50 + "\n") * 20)
|
||||
|
||||
Haiku.request_haiku(
|
||||
"apple banana papaya", url=httpserver.url_for("/testhaiku"))
|
||||
|
|
@ -3,11 +3,21 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import pytest # noqa: F401
|
||||
import os
|
||||
|
||||
from senju.haiku import Haiku
|
||||
from senju.haiku import DEFAULT_HAIKU, Haiku
|
||||
from senju.store_manager import StoreManager # noqa: F401
|
||||
|
||||
|
||||
def test_temp_data_dir(temp_data_dir):
|
||||
print(temp_data_dir)
|
||||
testpath = temp_data_dir / "__test"
|
||||
with open(testpath, "w") as f:
|
||||
f.write("that dir actually works")
|
||||
os.remove(testpath)
|
||||
assert not os.path.exists(testpath)
|
||||
|
||||
|
||||
def test_save_and_load_any(store_manager: StoreManager):
|
||||
thing = {
|
||||
"color": "blue",
|
||||
|
|
@ -36,9 +46,50 @@ def test_save_and_load_haiku(store_manager: StoreManager):
|
|||
but should have"
|
||||
|
||||
assert h == h_loaded
|
||||
assert h != DEFAULT_HAIKU
|
||||
|
||||
|
||||
def test_load_latest_with_empty_store(temp_data_dir):
|
||||
store = StoreManager(temp_data_dir / "empty_store.json")
|
||||
h = store.get_id_of_latest_haiku()
|
||||
assert h is None
|
||||
|
||||
|
||||
def test_load_latest_or_default_with_empty(temp_data_dir):
|
||||
store = StoreManager(temp_data_dir / "load_or_default_empty.json")
|
||||
haiku = store.load_haiku(store.get_id_of_latest_haiku())
|
||||
assert haiku == DEFAULT_HAIKU
|
||||
|
||||
|
||||
def test_load_latest_or_default_with_non_empty(temp_data_dir):
|
||||
store = StoreManager(temp_data_dir / "load_or_default_not_empty.json")
|
||||
nonsense_test_haiku = Haiku(["nonsense", "test", "haiku"])
|
||||
store.save_haiku(nonsense_test_haiku)
|
||||
haiku = store.load_haiku(store.get_id_of_latest_haiku())
|
||||
assert haiku != DEFAULT_HAIKU
|
||||
assert haiku == nonsense_test_haiku
|
||||
|
||||
|
||||
def test_load_latest_with_non_empty_store(temp_data_dir):
|
||||
store = StoreManager(temp_data_dir / "empty_store.json")
|
||||
store.save_haiku(Haiku(["hello", "world", "bananenkrokodil"]))
|
||||
h = store.get_id_of_latest_haiku()
|
||||
assert h is not None
|
||||
assert h > 0
|
||||
|
||||
|
||||
def test_create_store_with_bad_file(temp_data_dir):
|
||||
with pytest.raises(Exception):
|
||||
testpath = temp_data_dir / "non_empty.json"
|
||||
with open(testpath, "w") as f:
|
||||
f.write("BUT IT DOES NOT ACTUALLY HAVE JSON")
|
||||
store = StoreManager(testpath)
|
||||
store._save({"hello": 19})
|
||||
|
||||
|
||||
def test_create_store_with_non_empty(temp_data_dir):
|
||||
testpath = temp_data_dir / "non_empty.json"
|
||||
with open(testpath, "w") as f:
|
||||
f.write('{"this": ["is","valid","json"]}')
|
||||
store = StoreManager(testpath)
|
||||
store._save({"hello": 19})
|
||||
|
|
|
|||
|
|
@ -1,30 +0,0 @@
|
|||
# I do not trust python and it's tests, so I'm testing them. May not be worth
|
||||
# much, but at least it shows me a few things.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
# do not remove this import. This is needed for
|
||||
# pytest fixtures to work
|
||||
import pytest # noqa: F401
|
||||
|
||||
import senju # noqa: F401
|
||||
|
||||
# Note: these weird arguments are an indicator of what should be dome
|
||||
# before. For example, `temp_data_dir` is a function in `conftest.py`. If we
|
||||
# put it in the arguments, it seems to run before our test, and the return
|
||||
# value becomes a local. This is all very confusing for someone used to
|
||||
# Rust's libtest
|
||||
|
||||
|
||||
def test_tests_are_loaded():
|
||||
assert True # if we make it here, they are
|
||||
|
||||
|
||||
def test_temp_data_dir(temp_data_dir):
|
||||
print(temp_data_dir)
|
||||
testpath = temp_data_dir / "__test"
|
||||
with open(testpath, "w") as f:
|
||||
f.write("that dir actually works")
|
||||
os.remove(testpath)
|
||||
Loading…
Add table
Add a link
Reference in a new issue