SQL(MySQL用 schema.sql)

CREATE DATABASE IF NOT EXISTS deepseeks
  DEFAULT CHARACTER SET utf8mb4
  COLLATE utf8mb4_unicode_ci;

USE deepseeks;

-- シリアル番号管理テーブル(常に1行だけを保持)
CREATE TABLE IF NOT EXISTS serials (
  id INT PRIMARY KEY,
  current_serial INT NOT NULL
);

-- 初期値(0001スタートなど任意)
INSERT INTO serials (id, current_serial)
VALUES (1, 1)
ON DUPLICATE KEY UPDATE current_serial = current_serial;

-- トークンログテーブル
CREATE TABLE IF NOT EXISTS logs (
  id INT AUTO_INCREMENT PRIMARY KEY,
  serial_int INT NOT NULL,
  serial_str VARCHAR(4) NOT NULL,
  title VARCHAR(255) NOT NULL,
  prompt_tokens INT NOT NULL,
  completion_tokens INT NOT NULL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

Python: src/db/connection.py(MySQL接続)

import pymysql

DB_CONFIG = {
    "host": "localhost",
    "user": "deepseeks",
    "password": "deepseeks0000",
    "database": "deepseeks",
    "charset": "utf8mb4",
    "cursorclass": pymysql.cursors.DictCursor,
    "autocommit": True,
}


def get_connection():
    return pymysql.connect(**DB_CONFIG)

Python: src/services/serial_service.py(シリアル採番)

from db.connection import get_connection


def get_and_increment_serial():
    """
    serials.current_serial を取得し、その後 +1 して更新。
    戻り値は取得時点の値(例: 1 → '0001')。
    """
    conn = get_connection()
    try:
        with conn.cursor() as cur:
            # 排他制御(シンプルに FOR UPDATE)
            cur.execute("SELECT current_serial FROM serials WHERE id = 1 FOR UPDATE")
            row = cur.fetchone()
            if not row:
                raise RuntimeError("serials テーブルが初期化されていません。")

            current_serial = row["current_serial"]
            next_serial = current_serial + 1

            cur.execute(
                "UPDATE serials SET current_serial = %s WHERE id = 1",
                (next_serial,),
            )

        conn.commit()
        return current_serial
    finally:
        conn.close()


def format_serial(serial_int: int) -> str:
    """
    4桁ゼロ埋め文字列に変換(1 → '0001')
    """
    return f"{serial_int:04d}"

Python: src/services/log_service.py(トークンログ記録)

from db.connection import get_connection


def insert_log(serial_int: int, serial_str: str, title: str,
               prompt_tokens: int, completion_tokens: int):
    conn = get_connection()
    try:
        with conn.cursor() as cur:
            cur.execute(
                """
                INSERT INTO logs (serial_int, serial_str, title,
                                  prompt_tokens, completion_tokens)
                VALUES (%s, %s, %s, %s, %s)
                """,
                (serial_int, serial_str, title, prompt_tokens, completion_tokens),
            )
        conn.commit()
    finally:
        conn.close()

Python: src/services/file_service.py(履歴ファイル保存)

import os
from pathlib import Path

BASE_DIR = Path(__file__).resolve().parents[2]  # deepseeks/ を想定
PROMPTS_DIR = BASE_DIR / "src" / "prompts"
RESULTS_DIR = BASE_DIR / "src" / "results"

PROMPTS_DIR.mkdir(parents=True, exist_ok=True)
RESULTS_DIR.mkdir(parents=True, exist_ok=True)


def sanitize_title(title: str) -> str:
    """
    ファイル名に使えない文字を簡易除去/置換。
    必要に応じて強化。
    """
    bad_chars = ['/', '\\', ':', '*', '?', '"', '<', '>', '|']
    sanitized = title
    for ch in bad_chars:
        sanitized = sanitized.replace(ch, "_")
    return sanitized.strip() or "untitled"


def build_prompt_filename(serial_str: str, title: str) -> Path:
    safe_title = sanitize_title(title)
    filename = f"{serial_str}_prompt_{safe_title}.txt"
    return PROMPTS_DIR / filename


def build_result_filename(serial_str: str, title: str) -> Path:
    safe_title = sanitize_title(title)
    filename = f"{serial_str}_result_{safe_title}.txt"
    return RESULTS_DIR / filename


def save_text(path: Path, content: str):
    path.write_text(content, encoding="utf-8")

Python: src/services/deepseek_service.py(DeepSeek呼び出しラッパ・ダミー)

※ 実際の DeepSeek V4 API 仕様に合わせて書き換えてください。ここではダミー実装。

import os
import requests  # 実際のAPIに合わせて利用(不要なら削除)


DEEPSEEK_API_KEY = os.getenv("DEEPSEEK_API_KEY", "")
DEEPSEEK_API_URL = "https://api.deepseek.com/v4/chat/completions"  # 仮URL


def call_deepseek(prompt: str):
    """
    DeepSeek V4 を呼び出す想定のラッパ。
    戻り値: (reply_text, prompt_tokens, completion_tokens)
    """
    # TODO: 実際のAPI仕様に合わせて実装
    # ここではダミー応答とダミートークン数を返す
    dummy_reply = f"[DEEPSEEK REPLY]\n{prompt[:100]}..."
    dummy_prompt_tokens = max(1, len(prompt) // 4)
    dummy_completion_tokens = max(1, len(dummy_reply) // 4)
    return dummy_reply, dummy_prompt_tokens, dummy_completion_tokens

Python: src/flask_server.py(Flaskアプリ本体)

from flask import Flask, render_template, request
from services.serial_service import get_and_increment_serial, format_serial
from services.file_service import (
    build_prompt_filename,
    build_result_filename,
    save_text,
)
from services.log_service import insert_log
from services.deepseek_service import call_deepseek

app = Flask(
    __name__,
    template_folder="templates",
    static_folder="static_deepseeks",
)


@app.route("/", methods=["GET", "POST"])
def index():
    reply_text = None
    prompt_text = None
    title = None
    serial_str = None

    if request.method == "POST":
        title = request.form.get("title", "").strip()
        prompt_text = request.form.get("prompt", "").strip()

        if title and prompt_text:
            # シリアル取得&インクリメント
            serial_int = get_and_increment_serial()
            serial_str = format_serial(serial_int)

            # DeepSeek呼び出し
            reply_text, prompt_tokens, completion_tokens = call_deepseek(prompt_text)

            # ファイル保存
            prompt_path = build_prompt_filename(serial_str, title)
            result_path = build_result_filename(serial_str, title)
            save_text(prompt_path, prompt_text)
            save_text(result_path, reply_text)

            # トークンログ保存
            insert_log(
                serial_int=serial_int,
                serial_str=serial_str,
                title=title,
                prompt_tokens=prompt_tokens,
                completion_tokens=completion_tokens,
            )

    return render_template(
        "page.html",
        reply_text=reply_text,
        prompt_text=prompt_text,
        title_value=title,
        serial_str=serial_str,
    )


if __name__ == "__main__":
    app.run(debug=True)

HTML: src/templates/base.html

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>{% block title %}DeepSeeks Wrapper{% endblock %}</title>
    <link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}">
</head>
<body>
<header class="header">
    <h1>DeepSeek V4 Wrapper</h1>
</header>

<main class="main">
    {% block content %}{% endblock %}
</main>

<footer class="footer">
    <small>&copy; deepseeks wrapper</small>
</footer>
</body>
</html>

HTML: src/templates/page.html

{% extends "base.html" %}

{% block title %}
DeepSeeks Wrapper
{% endblock %}

{% block content %}
<section class="form-section">
    <form method="post">
        <div class="form-group">
            <label for="title">Title(page.htmlのtitleとしても使用)</label>
            <input type="text" id="title" name="title"
                   value="{{ title_value or '' }}" required>
        </div>

        <div class="form-group">
            <label for="prompt">Prompt</label>
            <textarea id="prompt" name="prompt" rows="10" required>{{ prompt_text or '' }}</textarea>
        </div>

        <div class="form-group">
            <button type="submit">Send to DeepSeek</button>
        </div>
    </form>
</section>

{% if reply_text %}
<section class="result-section">
    <h2>Result</h2>
    {% if serial_str %}
        <p class="serial-info">Serial: {{ serial_str }}</p>
    {% endif %}
    <pre class="result-pre">{{ reply_text }}</pre>
</section>
{% endif %}
{% endblock %}

CSS: src/static_deepseeks/style.css

body {
    margin: 0;
    font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
    background-color: #f5f5f7;
    color: #222;
}

.header {
    background-color: #111827;
    color: #f9fafb;
    padding: 1rem 2rem;
}

.header h1 {
    margin: 0;
    font-size: 1.4rem;
}

.main {
    max-width: 960px;
    margin: 2rem auto;
    padding: 0 1rem;
}

.form-section,
.result-section {
    background-color: #ffffff;
    border-radius: 8px;
    padding: 1.5rem;
    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
    margin-bottom: 1.5rem;
}

.form-group {
    margin-bottom: 1rem;
}

.form-group label {
    display: block;
    font-weight: 600;
    margin-bottom: 0.4rem;
}

.form-group input[type="text"],
.form-group textarea {
    width: 100%;
    box-sizing: border-box;
    padding: 0.5rem 0.6rem;
    border-radius: 4px;
    border: 1px solid #d1d5db;
    font-size: 0.95rem;
}

.form-group textarea {
    resize: vertical;
}

.form-group button {
    padding: 0.5rem 1.2rem;
    border-radius: 4px;
    border: none;
    background-color: #2563eb;
    color: #f9fafb;
    font-size: 0.95rem;
    cursor: pointer;
}

.form-group button:hover {
    background-color: #1d4ed8;
}

.result-pre {
    white-space: pre-wrap;
    background-color: #f3f4f6;
    padding: 1rem;
    border-radius: 4px;
    font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
    font-size: 0.9rem;
}

.serial-info {
    font-size: 0.85rem;
    color: #6b7280;
    margin-bottom: 0.5rem;
}

.footer {
    text-align: center;
    padding: 1rem 0;
    color: #6b7280;
    font-size: 0.8rem;
}

必要に応じて:

  • DEEPSEEK_API_URLcall_deepseek 内の実装を実際の DeepSeek V4 API 仕様に差し替え。

ここまでで、指定フォルダ構成・ファイル命名規則・MySQLログ・シリアル採番が一通りつながる形になっています。