Corin1998 commited on
Commit
9972f7c
·
verified ·
1 Parent(s): dca7b74

Upload 15 files

Browse files
# backend.yaml ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # backend
2
+ cd backend
3
+ python -m venv .venv && source .venv/bin/activate
4
+ pip install -r requirements.txt
5
+ cp .env.example .env # キーを設定
6
+ uvicorn app.main:app --reload
7
+
8
+ # frontend
9
+ cd ../frontend
10
+ npm i
11
+ npm run dev
README.md CHANGED
@@ -1,10 +1,8 @@
1
- ---
2
- title: Sales
3
- emoji: 📉
4
- colorFrom: pink
5
- colorTo: gray
6
- sdk: docker
7
- pinned: false
8
- ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
1
+ # 営業メール/提案書ジェネレーター(HF + ChatGPT)
 
 
 
 
 
 
 
2
 
3
+ ## 事前準備
4
+ - `backend/.env.example` を `backend/.env` にコピーし、`OPENAI_API_KEY` と `HF_API_KEY` を設定してください。
5
+
6
+ ## ローカル起動(Docker)
7
+ ```bash
8
+ docker compose up --build
backend:app:hf_utils.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import httpx
2
+ from .config import settings
3
+
4
+ HF_API_URL = "https://api-inference.huggingface.co/models"
5
+
6
+ async def hf_text_classification(model:str,text:str):
7
+ heders = {"authorization":f"Bearer{settings.hf_api_key}"}
8
+ async with httpx.AsyncClient(timeout=60) as client:
9
+ r = await client.post(f"{HF_API__URL}/{model}",headers=headers,json={"inputs":text})
10
+ r.raise_for_status()
11
+ return r.json()
12
+
13
+ async_def toxicity_score(text:str)->float:
14
+ try:
15
+ results = await hf_text_classification(settings.hf_toxic_model,text)
16
+
17
+ if isinstance(results,list)and results and isinstance(results[0],list):
18
+
19
+ labels = {d["label"].lower():d["score"]for d in results[0]}
20
+ return float (label.get("toxic",0.0))
21
+ elif isinstance(results,list)and results and "label" in results[0]:
22
+ return float(results[0]["score"])
23
+ except Exception:
24
+ pass
25
+ return 0.0
26
+
27
+ async def sentiment_polarity(text:str)->float:
28
+ try:
29
+ results = await hf_text_classification(settings.hf_sentiment_model)
30
+
31
+ if isinstance(results, list)and results and isinstance(result[0],list):
32
+ labels = {d["label"].upper():d["score"]for d in results[0]}
33
+ return float(labels.get("POSITIVE",0.0)-labels.get("NEAGATIVE",0.0))
34
+ elif isinstance(results,list)and results and "label" in results[0]:
35
+ label = results[0]["label"].upper()
36
+ score = results[0] ["score"]
37
+ return score if label == "POSITIVE" else (-score if label == "NEGATIVE" else 0.0)
38
+ except Exception:
39
+ pass
40
+ return 0.0
backend:app:main.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from .schemas import GenerateRequest, EmailResponse, PropasalResponse, LintRequest, LintResponse
4
+ from .prompt_teplates import EMAIL_SYSTEM, EMAIL_USER,PROPOSAL_SYSTEM,PROPOSAL_USER
5
+ from .openai_utils import chat_complete,split_subject_body
6
+ from .hf_utils import toxicity_score, sentiment_polarity
7
+
8
+ app = FastAPI(title="Salse Writer API", version ="0.1.0")
9
+
10
+ app.add_middleware(
11
+ CORMiddleware,
12
+ allow_origins=["http://localhost:5173"],
13
+ allow_credentials=True,
14
+ allow_methods=["*"],
15
+ allow_headers=["*"],
16
+ )
17
+
18
+ @app.get("/health")
19
+ def health():
20
+ rerutn{"status":"ok"}
21
+
22
+ @app.post("/generate/email",response_model=EmailResponse)
23
+ async def generate_email(req:GenerateRequest):
24
+ user_prompt = EMAIL_USER.format(**req.model_dump())
25
+ raw = chat_complete(EMAIL_SYSTEM,user_prompt)
26
+ subject,body =split_subject_body(raw)
27
+
28
+ tox = await toxicity_score(body)
29
+ sent = await sentiment_polarity(body)
30
+ warnings = []
31
+ if tox > 0.2:
32
+ warnings.append("トーンが攻撃的/不適切の可能性があります。表現を柔らかくしてください。")
33
+
34
+ quality={"toxicity":tox, "sentiment":sent}
35
+ return EmailResponse(subject=subject,body=body,quality=quality,warnings=warnings)
36
+
37
+ @app.post("/generate/proposal",response_model=ProposalResponse)
38
+ async def generate_proposal(req:GenerateRequest):
39
+ user_prompt = PROPOSAL_USER.format(**req.model_dump())
40
+ text = chat_complete(PROPOSAL_SYSTEM,user_prompt)
41
+
42
+ lines=[ln.strip("-• ").strip() for ln in text.splitlines()if ln.strip()]
43
+ outline = [ ln for ln in lines if not ln.startswith("エグゼクティブサマリー")]
44
+ summary=""
45
+ for i,ln in enumerate(lines):
46
+ if "エグゼクティブサマリー"in ln:
47
+ summary ="\n".join(lines[i+1:])
48
+ break
49
+ return ProposalResponse(outline=outline[:8],exective_summary=summary or "(サマリー抽出に失敗しました)")
50
+
51
+ @app.post("/lint", response_model=LintResponse)
52
+ async def lint_text(req:LintRequest):
53
+ tox =await toxicity_score(req.text)
54
+ issues=[]
55
+ if tox > 0.2:
56
+ issues.append("不適切・攻撃的な表現を含む可能性")
57
+
58
+ bad_patterns=["絶対に","必ず儲かる","今だけ","無料で全部"]
59
+ if any(p in req_text for p in bad_patterns):
60
+ issues.append("誇大広告とみなされる恐れのある表現")
61
+ return LintResponse(issues=issues, toxicity=tox)
docker-compose.yml ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ services:
2
+ api:
3
+ build: ./backend
4
+ env_file:
5
+ - ./backend/.env.example
6
+ ports:
7
+ - "8000:8000"
8
+ web:
9
+ build: ./frontend
10
+ environment:
11
+ - VITE_API_BASE=http://localhost:8000
12
+ ports:
13
+ - "5173:5173"
14
+ depends_on:
15
+ - api
frontend:Dockerfile ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ FROM node:20-slim
2
+ WORKDIR /app
3
+ COPY package.json package-lock.json* ./
4
+ RUN npm install
5
+ COPY . .
6
+ EXPOSE 5173
7
+ CMD ["npm", "run", "dev", "--", "--host", "0.0.0.0"]
frontend:package.json ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "sales-writer-frontend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview --port 5173"
10
+ },
11
+ "dependencies": {
12
+ "react": "^18.3.1",
13
+ "react-dom": "^18.3.1",
14
+ "zod": "^3.23.8"
15
+ },
16
+ "devDependencies": {
17
+ "@types/react": "^18.3.3",
18
+ "@types/react-dom": "^18.3.0",
19
+ "typescript": "^5.5.4",
20
+ "vite": "^5.4.2"
21
+ }
22
+ }
frontend:scr:components:Field.ts ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+
3
+ type Props = React.PropsWithChildren<{
4
+ label: string;
5
+ }>;
6
+
7
+ export default function Field({ label, children }: Props) {
8
+ return (
9
+ <label style={{ display: "block", marginBottom: 12 }}>
10
+ <div style={{ fontSize: 12, opacity: 0.8, marginBottom: 6 }}>{label}</div>
11
+ {children}
12
+ </label>
13
+ );
14
+ }
frontend:scr:components:GeneratorForm.tsx ADDED
@@ -0,0 +1,103 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from "react";
2
+ import Field from "./Field";
3
+ import { generateEmail, lintText, GenerateReq } from "@/lib/api";
4
+
5
+ type Props = {
6
+ onResult: (r: any) => void;
7
+ };
8
+
9
+ export default function GeneratorForm({ onResult }: Props) {
10
+ const [loading, setLoading] = useState(false);
11
+ const [form, setForm] = useState<GenerateReq>({
12
+ language: "ja",
13
+ industry: "ITサービス",
14
+ target_company: "株式会社○○",
15
+ target_persona: "情報システム部の部長",
16
+ pain_points: "SaaS乱立によるコスト・運用負荷増大",
17
+ value_prop: "運用統合と可視化によりTCOを30%削減",
18
+ product_name: "Aroundabout Suite",
19
+ cta: "15分のオンライン面談をご提案",
20
+ tone: "フォーマルで簡潔",
21
+ variables: { company: "株式会社○○", person: "山田様" },
22
+ length_hint: "中",
23
+ extra_instr: ""
24
+ });
25
+
26
+ const update = (k: keyof GenerateReq) => (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement>) =>
27
+ setForm(f => ({ ...f, [k]: e.target.value }));
28
+
29
+ async function onSubmit(e: React.FormEvent) {
30
+ e.preventDefault();
31
+ setLoading(true);
32
+ try {
33
+ const res = await generateEmail({ ...form, variables: form.variables });
34
+ // 追加の簡易Lint
35
+ const lint = await lintText(res.body);
36
+ onResult({ ...res, lint });
37
+ } catch (err) {
38
+ onResult({ error: String(err) });
39
+ } finally {
40
+ setLoading(false);
41
+ }
42
+ }
43
+
44
+ return (
45
+ <form onSubmit={onSubmit} style={{ display: "grid", gap: 12 }}>
46
+ <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr", gap: 12 }}>
47
+ <Field label="言語">
48
+ <select value={form.language} onChange={update("language")}>
49
+ <option value="ja">日本語</option>
50
+ <option value="en">English</option>
51
+ </select>
52
+ </Field>
53
+ <Field label="トーン">
54
+ <input value={form.tone} onChange={update("tone")} />
55
+ </Field>
56
+ <Field label="業界">
57
+ <input value={form.industry} onChange={update("industry")} />
58
+ </Field>
59
+ <Field label="相手企業">
60
+ <input value={form.target_company} onChange={update("target_company")} />
61
+ </Field>
62
+ <Field label="担当者ペルソナ">
63
+ <input value={form.target_persona} onChange={update("target_persona")} />
64
+ </Field>
65
+ <Field label="文章ボリューム">
66
+ <select value={form.length_hint} onChange={update("length_hint")}>
67
+ <option>短</option><option>中</option><option>長</option>
68
+ </select>
69
+ </Field>
70
+ </div>
71
+
72
+ <Field label="相手の課題">
73
+ <textarea value={form.pain_points} onChange={update("pain_points")} rows={3} />
74
+ </Field>
75
+ <Field label="提供価値(成果/差別化)">
76
+ <textarea value={form.value_prop} onChange={update("value_prop")} rows={3} />
77
+ </Field>
78
+ <Field label="製品/サービス名">
79
+ <input value={form.product_name} onChange={update("product_name")} />
80
+ </Field>
81
+ <Field label="CTA(次アクション)">
82
+ <input value={form.cta} onChange={update("cta")} />
83
+ </Field>
84
+ <Field label="追加指示(禁止表現など)">
85
+ <input value={form.extra_instr ?? ""} onChange={update("extra_instr")} />
86
+ </Field>
87
+
88
+ <button
89
+ type="submit"
90
+ disabled={loading}
91
+ style={{
92
+ padding: "10px 14px",
93
+ borderRadius: 12,
94
+ border: "1px solid #ddd",
95
+ cursor: "pointer",
96
+ background: loading ? "#f3f3f3" : "white"
97
+ }}
98
+ >
99
+ {loading ? "生成中..." : "生成する"}
100
+ </button>
101
+ </form>
102
+ );
103
+ }
frontend:scr:lib:api.ts ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export type GenerateReq = {
2
+ language: string;
3
+ industry: string;
4
+ target_company: string;
5
+ target_persona: string;
6
+ pain_points: string;
7
+ value_prop: string;
8
+ product_name: string;
9
+ cta: string;
10
+ tone: string;
11
+ variables: Record<string, string>;
12
+ length_hint: string;
13
+ extra_instr?: string | null;
14
+ };
15
+
16
+ const BASE = import.meta.env.VITE_API_BASE ?? "http://localhost:8000";
17
+
18
+ export async function generateEmail(payload: GenerateReq) {
19
+ const r = await fetch(`${BASE}/generate/email`, {
20
+ method: "POST",
21
+ headers: { "Content-Type": "application/json" },
22
+ body: JSON.stringify(payload),
23
+ });
24
+ if (!r.ok) throw new Error(await r.text());
25
+ return r.json();
26
+ }
27
+
28
+ export async function lintText(text: string) {
29
+ const r = await fetch(`${BASE}/lint`, {
30
+ method: "POST",
31
+ headers: { "Content-Type": "application/json" },
32
+ body: JSON.stringify({ text }),
33
+ });
34
+ if (!r.ok) throw new Error(await r.text());
35
+ return r.json();
36
+ }
frontend:src:App.tsx ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React, { useState } from "react";
2
+ import GeneratorForm from "./components/GeneratorForm";
3
+ import OutputPanel from "./components/OutputPanel";
4
+
5
+ export default function App() {
6
+ const [data, setData] = useState<any | null>(null);
7
+
8
+ return (
9
+ <div style={{ maxWidth: 980, margin: "0 auto", padding: 24 }}>
10
+ <header style={{ marginBottom: 18 }}>
11
+ <h1 style={{ margin: 0 }}>営業メール / 提案書ジェネレーター</h1>
12
+ <div style={{ opacity: 0.7 }}>Hugging Face + ChatGPT(API連携MVP)</div>
13
+ </header>
14
+ <GeneratorForm onResult={setData} />
15
+ <OutputPanel data={data} />
16
+ <footer style={{ marginTop: 24, fontSize: 12, opacity: 0.6 }}>
17
+ ※入力内容は生成のみに使用されます。保存は行いません(デモ設定)。
18
+ </footer>
19
+ </div>
20
+ );
21
+ }
frontend:src:components:OutputPanel.tsx ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+
3
+ type Props = {
4
+ data: any | null;
5
+ };
6
+
7
+ export default function OutputPanel({ data }: Props) {
8
+ if (!data) return null;
9
+ if (data.error) return <div style={{ color: "crimson" }}>Error: {data.error}</div>;
10
+
11
+ return (
12
+ <div style={{ marginTop: 16, padding: 16, border: "1px solid #eee", borderRadius: 12 }}>
13
+ <h3 style={{ marginTop: 0 }}>結果</h3>
14
+ <div><strong>件名:</strong> {data.subject}</div>
15
+ <div style={{ whiteSpace: "pre-wrap", marginTop: 8 }}>{data.body}</div>
16
+ <div style={{ marginTop: 12, opacity: 0.8 }}>
17
+ <div>toxicity: {data.quality?.toxicity?.toFixed?.(3)}</div>
18
+ <div>sentiment: {data.quality?.sentiment?.toFixed?.(3)}</div>
19
+ {data.warnings?.length ? (
20
+ <ul>{data.warnings.map((w: string, i: number) => <li key={i}>{w}</li>)}</ul>
21
+ ) : null}
22
+ {data.lint?.issues?.length ? (
23
+ <>
24
+ <div style={{ marginTop: 8 }}><strong>Lint:</strong></div>
25
+ <ul>{data.lint.issues.map((w: string, i: number) => <li key={i}>{w}</li>)}</ul>
26
+ </>
27
+ ) : null}
28
+ </div>
29
+ </div>
30
+ );
31
+ }
frontend:src:main.tsx ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { createRoot } from "react-dom/client";
3
+ import App from "./App";
4
+
5
+ createRoot(document.getElementById("root")!).render(<App />);
frontend:tsconfig.json ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
5
+ "jsx": "react-jsx",
6
+ "module": "ESNext",
7
+ "moduleResolution": "Bundler",
8
+ "strict": true,
9
+ "baseUrl": ".",
10
+ "paths": { "@/*": ["src/*"] }
11
+ },
12
+ "include": ["src"]
13
+ }
frontend:vite.cofig.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import{defineConfig}from "vite";
2
+ import react from "@vitejs/plugin-react";
3
+
4
+ export default defineConfig({
5
+ plugins:[react()],
6
+ server:{port:5173},
7
+ });