fix_and_resume_batch.py 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. import asyncio
  2. import json
  3. import sqlite3
  4. import subprocess
  5. import sys
  6. import urllib.request
  7. from pathlib import Path
  8. DB = Path("data/telephony.sqlite3")
  9. MODEL = "qwen3:8b"
  10. OLLAMA = "http://127.0.0.1:11434/api/generate"
  11. def schema():
  12. return {
  13. "type": "object",
  14. "properties": {
  15. "customer": {
  16. "type": "object",
  17. "properties": {
  18. "name": {"type": ["string", "null"]},
  19. "email": {"type": ["string", "null"]},
  20. "phone": {"type": ["string", "null"]},
  21. "address": {"type": ["string", "null"]},
  22. "customer_number": {"type": ["string", "null"]},
  23. "order_number": {"type": ["string", "null"]}
  24. },
  25. "required": [
  26. "name", "email", "phone", "address",
  27. "customer_number", "order_number"
  28. ]
  29. },
  30. "products": {
  31. "type": "array",
  32. "items": {
  33. "type": "object",
  34. "properties": {
  35. "raw_text": {"type": "string"},
  36. "normalized": {"type": ["string", "null"]},
  37. "quantity": {"type": ["number", "null"]},
  38. "uncertain": {"type": "boolean"}
  39. },
  40. "required": [
  41. "raw_text", "normalized",
  42. "quantity", "uncertain"
  43. ]
  44. }
  45. },
  46. "vouchers": {
  47. "type": "array",
  48. "items": {
  49. "type": "object",
  50. "properties": {
  51. "raw_text": {"type": "string"},
  52. "amount": {"type": ["number", "null"]},
  53. "code": {"type": ["string", "null"]},
  54. "uncertain": {"type": "boolean"}
  55. },
  56. "required": [
  57. "raw_text", "amount",
  58. "code", "uncertain"
  59. ]
  60. }
  61. },
  62. "analysis": {
  63. "type": "object",
  64. "properties": {
  65. "intent": {"type": ["string", "null"]},
  66. "sentiment": {
  67. "type": ["string", "null"],
  68. "enum": ["positive", "neutral", "negative", "mixed", None]
  69. },
  70. "summary": {"type": ["string", "null"]},
  71. "advice": {
  72. "type": "array",
  73. "items": {"type": "string"}
  74. },
  75. "follow_up": {
  76. "type": "array",
  77. "items": {"type": "string"}
  78. }
  79. },
  80. "required": [
  81. "intent", "sentiment", "summary",
  82. "advice", "follow_up"
  83. ]
  84. },
  85. "uncertainties": {
  86. "type": "array",
  87. "items": {"type": "string"}
  88. }
  89. },
  90. "required": [
  91. "customer",
  92. "products",
  93. "vouchers",
  94. "analysis",
  95. "uncertainties"
  96. ]
  97. }
  98. def transcript_text(data):
  99. texts = []
  100. for item in data.get("transcripts", []):
  101. for segment in item.get("segments", []):
  102. text = segment.get("text", "").strip()
  103. if text:
  104. texts.append(text)
  105. return "\n".join(texts)
  106. def analyze(text):
  107. prompt = f"""
  108. Analysiere dieses deutschsprachige Kundentelefonat.
  109. Extrahiere personenbezogene Daten, sofern sie im Gespräch genannt werden:
  110. Name, E-Mail, Telefonnummer, Adresse, Kundennummer, Bestellnummer
  111. und Gutscheincode.
  112. Diese Daten dienen der späteren Kunden- und Auftragszuordnung.
  113. Verwende ausschließlich Informationen aus dem Transkript.
  114. Erfinde niemals Daten.
  115. Whisper kann Wörter falsch erkennen.
  116. Korrigiere einen Fehler nur, wenn der Kontext die Korrektur eindeutig macht.
  117. Bei unsicheren Produktnamen, Nummern, Codes oder Namen:
  118. raw_text = tatsächlich erkannter Wortlaut
  119. normalized = null
  120. uncertain = true
  121. Keine Sprecherzuordnung erfinden.
  122. Keine neuen Zeitstempel erzeugen.
  123. TRANSKRIPT:
  124. {text}
  125. """
  126. payload = {
  127. "model": MODEL,
  128. "prompt": prompt,
  129. "stream": False,
  130. "format": schema(),
  131. "think": False,
  132. "options": {"temperature": 0}
  133. }
  134. request = urllib.request.Request(
  135. OLLAMA,
  136. data=json.dumps(payload).encode(),
  137. headers={"Content-Type": "application/json"},
  138. method="POST"
  139. )
  140. with urllib.request.urlopen(request, timeout=300) as response:
  141. return json.loads(json.load(response)["response"])
  142. async def main():
  143. con = sqlite3.connect(DB)
  144. con.row_factory = sqlite3.Row
  145. t = con.execute("""
  146. SELECT *
  147. FROM transcripts
  148. WHERE rec_id = 4618
  149. ORDER BY id DESC
  150. LIMIT 1
  151. """).fetchone()
  152. if not t:
  153. raise RuntimeError("Transcript 4618 nicht gefunden.")
  154. print(f"Vorhandenes Transcript: {t['id']}")
  155. data = json.loads(t["transcript_json"])
  156. text = transcript_text(data)
  157. print(f"Transkript: {len(text)} Zeichen")
  158. print("Qwen3:8b analysiert vorhandenes Transkript ...")
  159. analysis = await asyncio.to_thread(analyze, text)
  160. con.execute("""
  161. INSERT INTO analyses (
  162. call_id,
  163. cdr_row_id,
  164. transcript_id,
  165. model,
  166. schema_version,
  167. analysis_json,
  168. status
  169. )
  170. VALUES (
  171. NULL,
  172. ?,
  173. ?,
  174. ?,
  175. '1.0',
  176. ?,
  177. 'completed'
  178. )
  179. """, (
  180. t["cdr_row_id"],
  181. t["id"],
  182. MODEL,
  183. json.dumps(analysis, ensure_ascii=False)
  184. ))
  185. con.commit()
  186. print()
  187. print("Analyse gespeichert.")
  188. print(json.dumps(analysis, ensure_ascii=False, indent=2))
  189. con.close()
  190. asyncio.run(main())